(версия 2, с исправленными опечатками — надеюсь, что всеми...)

Привет, Хабр!

Сейчас вот сидел, делал для товарища прототип генетического алгоритма. Навеяло поделиться оным, да и некоторыми другими мыслями…

Дано (клиент заказал): в некоем царстве складе есть 100 ячеек. В нем товар лежит. Как взять количество Х так, чтобы опустошить все задействованные ячейки до конца? Ну то есть, есть у вас типа ячейки [34, 10, 32, 32, 33, 41, 44, 4, 28, 23, 22, 28, 29, 14, 28, 15, 38, 49, 30, 24, 18, 9, 15, 8, 17, 9, 2, 7, 30, 29, 29, 2, 28, 23, 19, 4, 15, 30, 38, 3, 14, 21, 43, 50, 29, 14, 17, 12, 25, 15, 23, 28, 47, 38, 29, 7, 36, 45, 25, 6, 25, 11, 10, 1, 19, 37, 24, 27, 50, 12, 1, 1, 44, 22, 48, 13, 46, 49, 11, 33, 29, 4, 19, 33, 12, 3, 47, 27, 26, 45, 40, 37, 21, 2, 8, 41, 5, 1, 9, 5] — как набрать, скажем, 40

Ну можно перебором, наверняка есть что-то умное, а можно генетическим алгоритмом это решить…


Первый вопрос: а чего мозги сушить, ведь если просто идти по списку, по для этого надо взять из первых двух ячеек — во второй, правда, окажется остаток. И ходить далеко не надо будет. Но видать, клиент перфекционист, хочет, чтоб было, как в аптеке. Наверное, в складе ячейки на вес золота. Бывает.

Второй вопрос: а если отсортировать по возрастающей, то наверняка можно повычистить гораздо больше ячеек… В нашем примере полно ячеек с количеством меньше десяти. Наверное, тоже не хочет клиент ;) Мне тоже такие попадались: я тут начальник. Тебе сказали — делай, вопросов не задавай.

(Хорошо, не мой клиент, а то б я в очередной раз веру в человеческий разум потерял...)

Ну да Бог с ним, у каждого свои приоритеты… Тогда про генетические алгоритмы поговорим — надо ж это как-то решать… Писать будем на Яве.

Для тех, кто про них раньше толком не слышал: имитируем Мать Природу.
  1. Кодируем варианты поведения
  2. Проверяем, насколько хорошо работает каждый вариант с помощью подходящего бенчмарка
  3. Самые лучшие передают свои признаки следующему поколению


В последнем шаге в природе есть две составляющие: кроссинг-овер (обмен признаками между одинаковыми участками хромосомы) и мутация (спонтанное изменения генетического кода). Привет, средняя школа ;)

Вот, пожалуй, и всё. Кодировать будем, из каких ячеек брать, а из каких нет. У нас 100 ячеек, значит наша хромосома будет из 100 элементов true/false, я для этого взял String, в котором будут записаны ноли и единицы. Их, понятно, будет 100.

Бенчмарк — важнейшая вещь в этом процессе. Природа ищет нишу, а компьютерная природа будет искать дыру в вашем бенчмарке, чтобы обдурить его и выжить. Диву даешься, честное слово…

С учетом всего сказанного, примерно так:

private static int benchmark(String chromosome, boolean verbose) {
	int sum = 0;
	char[] cArr = chromosome.toCharArray();

	for (int i = 0; i < cnt_bins; i++) {
		if (cArr[i] == '1') {
			sum += stock[i];
			if (verbose)
				System.out.println("storage bin " + i + ":\t+" + stock[i] + "\t= " + sum);

		}
		if (sum == target_qty) {
			return 0;
		}
	}

	return Math.abs(target_qty - sum);
}


Идея — чем дальше мы от искомого числа 40, тем хуже. Если набрали 40 — дальше во хромосоме не идем, мы победили. Сортировать будем, понятно, по возрастающей — чем меньше штрафных очков, тем лучше.

Первое поколение создаем случайно:

// create 1st generation
		for (int i = 0; i < generation_size; i++) {
			StringWriter sw = new StringWriter();
			for (int j = 0; j < cnt_bins; j++) {
				// take stock from this bin?
				sw.append(rnd.nextBoolean() ? "1" : "0");
			}
			chromosomes.add(sw.toString());
			sw.close();
		}


Поскольку генетический алгоритм, вообще-то, приближается к цели, но не всегда ее достигает, важно ограничить количество поколений.

for (int generationNo = 0; generationNo < maxGenerationCnt; generationNo++) {
	// evaluate
	List<SimpleEntry<String, Integer>> evaluated = new ArrayList<>();
	for (int i = 0; i < chromosomes.size(); i++) {
		evaluated.add(
			new SimpleEntry<String, Integer>(chromosomes.get(i), benchmark(chromosomes.get(i), false)));
	} 
// ... тут будет остальной код, см. ниже ...
}
System.out.println("No solution found after " + maxGenerationCnt + " iterations");


Отсортируем, оставим лучшие:

...
// sort
evaluated = evaluated.stream().sorted((e1, e2) -> Integer.compare(e1.getValue(), e2.getValue()))
					.collect(Collectors.toList());

System.out.println("Generation " + generationNo + "; Best / last parent / worst: "
		+ evaluated.get(0).getValue() + " / " + evaluated.get(best_size).getValue() + " / "
		+ evaluated.get(evaluated.size() - 1).getValue());

if (evaluated.get(0).getValue() == 0) {
	System.out.println("Solution found");
	benchmark(evaluated.get(0).getKey(), true);
	System.exit(0);
}

// leave only the best
evaluated = evaluated.subList(0, best_size);
...


Плодимся и размножаемся, восстанавливаем размер популяции. То есть, выбираем родителей случайным образом, комбинируем одинаковые признаки (если повезет — см. флаг exchange), мутируем (флаг mutation), ждем чуда…

// replication
List<String> parents = evaluated.stream().map(e -> e.getKey()).collect(Collectors.toList());
chromosomes.clear();

while (chromosomes.size() < generation_size) {
	int parent1 = rnd.nextInt(evaluated.size());
	int parent2 = rnd.nextInt(evaluated.size());
	char[] cArr1 = parents.get(parent1).toCharArray();
        char[] cArr2 = parents.get(parent2).toCharArray();

	for (int i = 0; i < cnt_bins; i++) {
		boolean exchange = rnd.nextDouble() < exchange_rate;
		if (exchange) {
			char c1 = cArr1[i];
			char c2 = cArr2[i];

			// exchange both values
			cArr1[i] = c2;
			cArr2[i] = c1;
		}

		// mutation
		boolean mutation = rnd.nextDouble() < mutation_rate;
		if (mutation) {
			cArr1[i] = rnd.nextBoolean() ? '1' : '0';
		}

		mutation = rnd.nextDouble() < mutation_rate;
		if (mutation) {
			cArr2[i] = rnd.nextBoolean() ? '1' : '0';
		}
	}

	chromosomes.add(new String(cArr1));
	chromosomes.add(new String(cArr2));
			}


Ну и вот вам чудо:
Target value: 40
Stock: [34, 10, 32, 32, 33, 41, 44, 4, 28, 23, 22, 28, 29, 14, 28, 15, 38, 49, 30, 24, 18, 9, 15, 8, 17, 9, 2, 7, 30, 29, 29, 2, 28, 23, 19, 4, 15, 30, 38, 3, 14, 21, 43, 50, 29, 14, 17, 12, 25, 15, 23, 28, 47, 38, 29, 7, 36, 45, 25, 6, 25, 11, 10, 1, 19, 37, 24, 27, 50, 12, 1, 1, 44, 22, 48, 13, 46, 49, 11, 33, 29, 4, 19, 33, 12, 3, 47, 27, 26, 45, 40, 37, 21, 2, 8, 41, 5, 1, 9, 5]
Generation 0; Best / last parent / worst: 705 / 991 / 1580
Generation 1; Best / last parent / worst: 576 / 846 / 1175
Generation 2; Best / last parent / worst: 451 / 722 / 1108
Generation 3; Best / last parent / worst: 0 / 613 / 904
Solution found
storage bin 7: +4 = 4
storage bin 10: +22 = 26
storage bin 13: +14 = 40


А вот и весь код
package ypk;

import java.io.IOException;
import java.io.StringWriter;
import java.util.AbstractMap.SimpleEntry;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

public class YPK {
	private static int generation_size = 1000;
	private static int best_size = 200;
	private static int cnt_bins = 100;
	private static int max_stock = 50;
	private static double exchange_rate = .2;
	private static double mutation_rate = .01;
	private static Random rnd = new Random();
	private static int target_qty = rnd.nextInt(cnt_bins * max_stock / 5); // some quantity
	private static List<String> chromosomes = new ArrayList<>();
	private static int maxGenerationCnt = 20;

	private static int[] stock = new int[cnt_bins];

	public static void main(String[] args) throws IOException {
		System.out.println("Target value: " + target_qty);

		// create sample stock
		stock = new int[cnt_bins];
		for (int i = 0; i < cnt_bins; i++) {
			stock[i] = rnd.nextInt(max_stock) + 1;
		}

		System.out.println("Stock: " + Arrays.toString(stock));

		// create 1st generation
		for (int i = 0; i < generation_size; i++) {
			StringWriter sw = new StringWriter();
			for (int j = 0; j < cnt_bins; j++) {
				// take stock from this bin?
				sw.append(rnd.nextBoolean() ? "1" : "0");
			}
			chromosomes.add(sw.toString());
			sw.close();
		}

		for (int generationNo = 0; generationNo < maxGenerationCnt; generationNo++) {

			// evaluate
			List<SimpleEntry<String, Integer>> evaluated = new ArrayList<>();
			for (int i = 0; i < chromosomes.size(); i++) {
				evaluated.add(
						new SimpleEntry<String, Integer>(chromosomes.get(i), benchmark(chromosomes.get(i), false)));
			}

			// sort
			evaluated = evaluated.stream().sorted((e1, e2) -> Integer.compare(e1.getValue(), e2.getValue()))
					.collect(Collectors.toList());

			System.out.println("Generation " + generationNo + "; Best / last parent / worst: "
					+ evaluated.get(0).getValue() + " / " + evaluated.get(best_size).getValue() + " / "
					+ evaluated.get(evaluated.size() - 1).getValue());

			if (evaluated.get(0).getValue() == 0) {
				System.out.println("Solution found");
				benchmark(evaluated.get(0).getKey(), true);
				System.exit(0);
			}

			// leave only the best
			evaluated = evaluated.subList(0, best_size);

			// replication
			List<String> parents = evaluated.stream().map(e -> e.getKey()).collect(Collectors.toList());
			chromosomes.clear();

			while (chromosomes.size() < generation_size) {
				int parent1 = rnd.nextInt(evaluated.size());
				int parent2 = rnd.nextInt(evaluated.size());
				char[] cArr1 = parents.get(parent1).toCharArray();
				char[] cArr2 = parents.get(parent2).toCharArray();

				for (int i = 0; i < cnt_bins; i++) {
					boolean exchange = rnd.nextDouble() < exchange_rate;
					if (exchange) {
						char c1 = cArr1[i];
						char c2 = cArr2[i];

						// exchange both values
						cArr1[i] = c2;
						cArr2[i] = c1;
					}

					// mutation
					boolean mutation = rnd.nextDouble() < mutation_rate;
					if (mutation) {
						cArr1[i] = rnd.nextBoolean() ? '1' : '0';
					}

					mutation = rnd.nextDouble() < mutation_rate;
					if (mutation) {
						cArr2[i] = rnd.nextBoolean() ? '1' : '0';
					}
				}

				chromosomes.add(new String(cArr1));
				chromosomes.add(new String(cArr2));
			}
		}
		System.out.println("No solution found after " + maxGenerationCnt + " iterations");
	}

	private static int benchmark(String chromosome, boolean verbose) {
		int sum = 0;
		char[] cArr = chromosome.toCharArray();

		for (int i = 0; i < cnt_bins; i++) {
			if (cArr[i] == '1') {
				sum += stock[i];
				if (verbose)
					System.out.println("storage bin " + i + ":\t+" + stock[i] + "\t= " + sum);

			}
			if (sum == target_qty) {
				return 0;
			}
		}

		return Math.abs(target_qty - sum);
	}

}



Ну и в завершение: если намутить с параметрами — слишком много мутаций, слишком маленький размер популяции и т.п. — будет топтаться на месте или даже давать все худшие результаты.

Задачка эта, кстати, часто решается уже случайным образом и следующие поколения не нужны :) Если хотите усложнить компьютеру жизнь — уберите вот этот return из бенчмарка:

if (sum == target_qty) {
	return 0;
}


Это усложнит задачу и заставит комп помучиться немного…

Удачи,

m_OO_m

Комментарии (5)


  1. roller
    02.09.2019 01:07

    Нуу эм… вроде бы при помощи ГА обычно подбирают сам алгоритм или настроечные параметры для него. Но не решают изначальную задачу, потому что это 1) непредсказуемо по времени 2) неустойчиво.


  1. gecube
    02.09.2019 03:15

    TL;DR. А если серьезно — подача материала такая, что, к сожалению, сложно понять о чем вообще идет речь. А очень жаль. Потому что тема очень интересная.


  1. orion76
    02.09.2019 05:28

    Непонятно, почему принято решение, что для данной задачи ГА — оптимален.
    Возможно в статье приведены не все «вводные данные» задачи.

    Больше похоже на разминку-развлечение для ума, чтобы «компу» жизнь мёдом не казалась.-)


  1. evkochurov
    02.09.2019 11:46

    Ну можно перебором, наверняка есть что-то умное

    Да даже без умного, совсем по-простому можно: https://jsbin.com/cozakeyude/1/edit?js,console


  1. avdx
    02.09.2019 12:02

    Как я понимаю, это вот эта задача:
    https://ru.wikipedia.org/wiki/Задача_о_сумме_подмножеств
    Она NP-полная. В статье приводятся имеющиеся алгоритмы решения. В том числе есть предлагается решения генетическими алгоритмами:
    Решение задачи о сумме подмножеств