Thread overview
2 Sepette Yer Değişen Elmalar
Nov 19, 2021
Salih Dincer
Nov 19, 2021
Ali Çehreli
Nov 20, 2021
Salih Dincer
November 19, 2021

Merhaba,

Aşağıdaki bir tür zeka oyunu değil! Sadece iş çıkışı std.algorithm olanakları üzerine denenmiş rasgelelik üzerine ve OOP olmayan basit bir kod...

Ama eğlenceli sanki, ne dersiniz? Elbet daha zeki şekilde de programlanabilir!

import std.random, std.range, std.stdio;
import std.algorithm.mutation : remove;
import std.algorithm.iteration : sum;
import std.math : abs;

/* En hafifi 90 gram, en ağırı 120 gram olan ve
 * 10 adet elma bulunduran 2 sepetimiz olsun:
 */
enum {       enAz = 90,
             enÇok = 120,
             adet = 10,
             değişim = 2
}

void main() {	// Sepetlerimizi öyle rasgele dolduruyoruz:
    auto sepet1 = generate!(() => uniform(enAz, enÇok)).
                                      takeExactly(adet).array;
																		
    auto sepet2 = generate!(() => uniform(enAz, enÇok)).
                                      takeExactly(adet).array;

	/* Herhangi bir sepetten ikişer elma eksilten ve
	 * bunları döndüren işlevimiz: avuçla() ise şöyle:
	 */
	auto avuçla(ref int[] sepet) {
    size_t pos = uniform(0, sepet.length);

    int ilk = sepet[pos];
    sepet = remove(sepet, pos);

    pos = uniform(0, sepet.length);
    scope(exit) sepet = remove(sepet, pos);

    return [ ilk, sepet[pos] ];
  }
	// Sonsuz döngü için test ederiz ama şartlı*:
	size_t loop = 1;
	do {
		auto x = sepet1.sum; auto y = sepet2.sum;
		int fark = abs (x - y);
		
		x.writeln(" gr. (sepet1)");
		y.writeln(" gr. (sepet2), fark:", fark);
		
		writefln("%s\n%s\n", sepet1, sepet2);
		
		if(fark < 4 ) {           // (*) Fark küçükse çık:
		  "Toplam rasgele değişim: ".writeln(loop);
		   break;
		}
		// Önce birinden 2 elma aldık ve hemen...
		auto birAvuçElma = avuçla(sepet2);
  		sepet2 ~= avuçla(sepet1); /*... diğer avcumuzla da
  		fazlaları alıp eksilen sepet attık! */

		// Avcumuzda kalan elmayı da diğerine ekliyoruz:
		sepet1 ~= birAvuçElma;    // sepet2'den ilk seçtiğimiz!
	} while(loop++);
}/* ÇIKTISI: (en az olasılık!)
1068 gr. (sepet1)
1070 gr. (sepet2), fark:2
[116, 114, 119, 110, 93, 98, 104, 103, 103, 108]
[110, 98, 119, 116, 101, 91, 107, 118, 93, 117]

Toplam rasgele değişim: 1*/
November 19, 2021
On 11/19/21 7:04 AM, Salih Dincer wrote:

> Elbet daha zeki şekilde de programlanabilir!

İki kaptaki sıvıları azar azar karıştırarak sonuçta aynı ortalama sıvıyı elde etmeye çalışıyor.

Programı kendim için irdeleyeceğim. Söylediklerimin hiçbirisi önemli değil; yalnızca aklıma gelenleri yazıyorum.

> /* En hafifi 90 gram, en ağırı 120 gram olan ve
>   * 10 adet elma bulunduran 2 sepetimiz olsun:
>   */
> enum {       enAz = 90,
>               enÇok = 120,
>               adet = 10,
>               değişim = 2
> }

O şekilde tanımlanmaları, hepsi aynı gruba dahil olmadıklarından doğal gelmedi. (Örneğin, Renkler gibi bir ilişkileri yok.) Ben böyle durumlarda tek tek tanımıyorum:

enum enAz = 90;
enum enÇok = 120;
// ...

Adet bildiren kavramları açıkça size_t olarak tanımlıyorum. (Yukarıdakilerin türü int'tir.)

Ama bu konuda bana katılmayan çok programcı var: Onlar, dizilerin .length'inin bilen işaretli bir tür olması gerektiğini savunurlar (örneğin long). Bunun nedeni, iki uzunluğunun farkını aldıklarında negatif sayı görmek isterler ama iki size_t'nin farkı negatif olmaz ve hatalara neden olabilir. İki tarafın da haklı olduğu çözülmez bir konu... :)

> void main() {    // Sepetlerimizi öyle rasgele dolduruyoruz:
>      auto sepet1 = generate!(() => uniform(enAz, enÇok)).
>                                        takeExactly(adet).array;

Leziz! :) uniform sondaki değeri hiç seçmez; en büyük değer 119 olacak. Onu da seçmesi istendiğinde şöyle çağrılabiliyor:

  uniform!"[]"(enAz, enÇok);

Köşeli parantez dahil anlamına gelir. uniform, "[)" varsayar.

takeExactly'nin take'ten farkı, istenen adet eleman bulunmadığında hata atmasıdır. (Az eleman varsa take hiç sesini çıkartmaz.) Burada ikisi de kullanılabilir çünkü generate'in ürettiği sonsuz dizide 'adet' adet eleman bulunacağından eminiz. :)

>      auto sepet2 = generate!(() => uniform(enAz, enÇok)).
>                                        takeExactly(adet).array;

Kod tekrarı! ;)

>      /* Herhangi bir sepetten ikişer elma eksilten ve
>       * bunları döndüren işlevimiz: avuçla() ise şöyle:
>       */
>      auto avuçla(ref int[] sepet) {
>      size_t pos = uniform(0, sepet.length);
>
>      int ilk = sepet[pos];
>      sepet = remove(sepet, pos);

Burada önemli değil ama eğer

1) Dizi çok büyükse
2) ve elemanların sırası önemli değilse,

remove'a eleman sıralarını korumadan çıkartmasını söyleyebiliriz:

- SwapStrategy.stable (varsayılan): Ortadan eleman çıkartınca sonraki bütün elemanları bir kaydırır; ortalamada N/2 kere kopyalar.

- SwapStrategy.unstable (bizim kullanabileceğimiz): En sondaki elemanı çıkan elemanın yerine kopyalar; tek kopya.

- SwapStrategy.semistable (haberim yoktu; sonradan eklenmiş olmalı): Bazı algoritmalarda yalnızca soldaki grubun sırasını korur.

Bu programı kendim yazarken burada daha ileri bir eniyileştirme yapılabileceğini farkettim: Neden sonuç dizisine eleman ekleyelim (bazı eklemelerde yeni bellek ayrılır), sürekli olarak 'sepet = remove/* ... */' yapalım?

Aklıma, elemanları baştan seçelim ve onları sonuç olarak kullanabilelim diye bir fikir geldi, "kesin vardır" dedim, ve std.random.partialShuffle'ı buldum. Kendi programımda kullanıyorum.

>      pos = uniform(0, sepet.length);
>      scope(exit) sepet = remove(sepet, pos);

Yani, "bir elma seç, sonra bir elma daha seç" diyorsun. :) Ben bunu "iki elma seç"ten daha karmaşık bulduğumdan aklıma bile gelmez. "Code review" sırasında karşılaşsam yazan kişiden basitleştirmesini isterim çünkü bu tür akıllı durumlar daha sonradan sorunlara neden olabilir.

>      return [ ilk, sepet[pos] ];
>    }
>      // Sonsuz döngü için test ederiz ama şartlı*:
>      size_t loop = 1;
>      do {

// ...

>      } while(loop++);

O da yanıltıcı olmuş çünkü loop 0 olana kadar devam edecekmiş gibi görünüyor ama asıl çıkış koşulu içeride bir yerde. Bu koşulun da etkisi olabilir gibi görünüyor ama 64 bitlik bir sistemde üstelik giriş/çıkış yapan bu program o koşula evrenin sonundan önce gelebilir mi? ;)

Bu durumda en iyisi, koşulsuz döngü:

  // Benim sevdiğim
  while (true) {
    // ...
  }

veya

  // Başka bazı programcıların sevdiği
  for (;;) {
    // ...
  }

Ama sen loop'u da yazdırdığına göre ben de for'u yeğliyorum. Ama yine de loop'un döngü sonundaki while tarafından arttırılması, bana gizli kalıyor gibi geldi.

>        auto x = sepet1.sum; auto y = sepet2.sum;

Ben değişkenleri ayrı satırlarda tanımlamayı seviyorum. Yoksa sağdakiler gizleniyorlar gibi geliyor. Ayrıca, bir el alışkanlığı olarak öncelikle 'const'ı seçiyorum.

Bunları uygulayınca ben aşağıdaki gibi değiştirdim. Aklıma swapRanges'den yararlanmak da geldi:

import std.random, std.range, std.stdio;
import std.algorithm.mutation : remove, swapRanges;
import std.algorithm.iteration : sum;
import std.math : abs;

enum enAz = 90;
enum enÇok = 120;
enum size_t adet = 10;
enum size_t değişim = 2;

auto rasgeleSepet() {
  return generate!(() => uniform!"[]"(enAz, enÇok))
         .takeExactly(adet)
         .array;
}

auto avuçla(ref int[] sepet) {
  // 'değişim' adet rasgele sayıyı başa getirir.
  // Tam istediğimiz şey! :)
  partialShuffle(sepet, değişim);

  // Baş tarafı sonuçtur
  auto sonuç = sepet[0..değişim];

  // Son tarafı da sepetin yeni durumu
  // sepet = sepet[değişim..$];

  // Yukarıdaki satırı, main'in sonundaki açıklamalara uygun
  // olarak çıkarttım.

  // Aslında artık partialShuffle'dan farkımız da kalmadı! :D

  return sonuç;
}

void main() {
  auto sepet1 = rasgeleSepet();
  auto sepet2 = rasgeleSepet();

  for (size_t loop = 0; ; loop++) {
    const x = sepet1.sum;
    const y = sepet2.sum;
    const fark = abs (x - y);

    x.writeln(" gr. (sepet1)");
    y.writeln(" gr. (sepet2), fark:", fark);

    writefln("%s\n%s\n", sepet1, sepet2);

    if(fark < 4 ) {
      "Toplam rasgele değişim: ".writeln(loop);
      break;
    }

    auto birAvuç = avuçla(sepet2);
    auto başkaAvuç = avuçla(sepet1);

    // Bu iki satırın yerine daha iyisini yazacağım. Açıklaması
    // aşağıda...
    //
    // sepet2 ~= avuçla(sepet1);
    // sepet1 ~= birAvuçElma;

    // Eğer bu programı hızlandırmak gerekirse, yukarıdaki ~=
    // işleçlerinden kurtulmak gerekecektir çünkü o işleç yeni
    // bellek ayırmak ve sonucu oraya kopyalamak zorundadır.
    //
    // Onun yerine, belki de sepetleri partialShuffle yaptıktan
    // sonra baş taraflarını yer değiştirmek daha hızlı
    // olabilir. Ama bu, probleme daha farklı yaklaşmayı
    // gerektirir.
    //
    //  Hattaaa... :) 'avuçla' işlevinin belgesine "döndürdüğü
    //  elemanları asıl dizinin baş tarafıdır" eklediğimiz an,
    //  burada std.algorithm.swapRanges'den yararlanabiliriz.

    swapRanges(birAvuç, başkaAvuç);

    // Bu mantığın doğru çalışabilmesi için avuçla'nın içindeki
    // 'sepet = sepet[değişim..$]' ifedesini çıkartttım.
  }
}

Ali


November 20, 2021

Öncelikle 2 düzeltme yapmalıyım:

Bunlardan ilki değişim enum'u hakkında. Aslında o limit anlamına gelen ve en az farkın kaç olduğunu belirten bir sabit sayı olacaktı. Sanırım bunu ilk programa kullanmayıp 4 sabitini kaldırmayı unutmuşum. Örneğin arzu edilen limit = 2 daha akıllıca çünkü bu sayede eşit olması mümkün bunu aşağıdaki programda göreceksiniz.

Diğeri ise zeki olması ile alakalı olan konu. Aslında biz aptalca bir döngüye sokarak hiçbir çözüm getirmiş olmuyoruz. Çünkü rasgele ne denk gelirse (tıpkı gerçek hayatta olduğu gibi) o seçiliyor ve her program çalıştığında farklı sonuç elde ediliyor. Bu yüzlerce sayılık bir döngü (loop) de olabilir, 3-5 hatta 1 seferde yani sadece ikişler elmayı yer değiştirerek de! İmkan olsaydı da bunu dizinin elemanlarını aradaki toplam farkı ile birlikte analiz eden akıllı bir yazılımla belki o bahsettiğim tek seferi bulabilirdik...

Zaten amacımız rasgelelik, yani gerçek hayatın bir yansımasını gözlemlemekti. Ben de bunu hem Ali hocanın ek std.algorithm özelliklerini (version 1), hem de önceki halin basit bir versiyon(2)'unu sabit 2 dizi ile test ettim. Elbette birbirine göre avantajları vardır. Her ikisi de çok güzeller ve iş görüyorlar:

import std.random, std.range, std.stdio;
import std.algorithm.mutation : remove, swapRanges;
import std.algorithm.iteration : sum;
import std.math : abs;
/*
 * version(1)'de 0 için 3 döngü, 42 için 48 defada dengelendi
 */
auto rnd = Random(42);
/*
 * version(2)'de 0 için 7 döngü, 42 için 20 defada dengelendi
 * YÜKSEK İHTİMALLE SİZDE DE BÖYLE OLACAK */
version = 2;

auto ver1(ref int[] arr) {
  partialShuffle(arr, 2, rnd);
  return arr[0..2];
}

auto ver2(ref int[] arr) {
  size_t pos = uniform(0, arr.length, rnd);

  int tmp = arr[pos];
      arr = arr.remove(pos);
      pos = uniform(0, arr.length, rnd);
  scope(exit) arr = arr.remove(pos);

  return [ tmp, arr[pos] ];
}

enum limit = 2;

void main() {
  auto s1 = [95, 92, 105, 97, 106, 111, 104, 104, 118, 104];
  auto s2 = [95, 100, 91, 109, 111, 114, 96, 117, 97, 92];

  foreach(n;0..99) {
    int gap = abs(s1.sum - s2.sum);
    gap.write(", ");
    //writefln("%s\n%s\n", s1, s2);
    if(gap < limit) {
      "\n".writeln("loop = ", n);
       break;
    }
version(1)    swapRanges(ver1(s2), ver1(s1));
version(2) {  auto tmp = ver2(s2);
                   s2 ~= ver2(s1);
                   s1 ~= tmp;//*/
           }
  }
}

Deneme

On Friday, 19 November 2021 at 17:48:28 UTC, Ali Çehreli wrote:

>

On 11/19/21 7:04 AM, Salih Dincer wrote:
değişim..$]' ifedesini çıkartttım.

>
 pos = uniform(0, sepet.length);
 scope(exit) sepet = remove(sepet, pos);

Yani, "bir elma seç, sonra bir elma daha seç"
diyorsun. :) Ben bunu "iki elma seç"ten daha
karmaşık bulduğumdan aklıma bile gelmez.
"Code review" sırasında karşılaşsam yazan
kişiden basitleştirmesini isterim çünkü bu
tür akıllı durumlar daha sonradan sorunlara
neden olabilir.

Aslında orada ekstra scope kullandığımdan, dönüş
değeri için result/sonuç gibi bir değişken (evet,
DMD bunu benim için arkaplanda yapıyordur)
kullanmadan basit bir çözüm tasarlamak. Yoksa kod
bize şu 3 şeyi söylüyor:

  • elma seç ve pos'un gösterdiği elmaya dokunma
  • ilk seçilen ile son seçileni aktarmayı unutma
  • return yapıldıktan sonra son seçileni kaldır
> >
 } while(loop++);

O da yanıltıcı olmuş çünkü loop 0 olana kadar
devam edecekmiş gibi görünüyor ama asıl çıkış
koşulu içeride bir yerde. Bu koşulun da etkisi
olabilir gibi görünüyor ama 64 bitlik bir
sistemde üstelik giriş/çıkış yapan bu program
o koşula evrenin sonundan önce gelebilir mi? ;)

Sabit dizi ve değişkenler "Random(41, 42)" ile
test edince, evet haklısın hocam. Çünkü sonsuz
döngüye ihtiyacımız yok hatta 100 adetten fazlası
gerçek hayatta anlamsız. İşin ilginci bu kodu
yazmadan evvel bizzat yaşadım. :)

Eşim 3 kilo elma almış ve bunu iki eşit parçaya
bölmemi istedi. Ama sayısı tekti ve elbette birini
yemek çözüm olabilirdi de helal değildi. Çünkü
diğer torbadaki Granysmith'ler başkasına 5 TL
karşılığında verilecekti...

Özetle 3 ya da 4 denemeden sonra ikişer değişim
ile her ikisini de 1.5 kiloya yakın tartmayı
başardım! Tabi bir torbada çift sayı (sanırım 16)
elma vardı ama kime denk geldiyse helal olsun!

Kalın sağlıcakla... (bu arada iyi yolculuklar!)