Vsevolod Levitan 3 місяців тому
батько
коміт
5b9416c8a7
3 змінених файлів з 61 додано та 45 видалено
  1. 32 25
      AntColony/Algorithm/ACO.cs
  2. 10 6
      AntColony/Algorithm/DefaultAnt.cs
  3. 19 14
      AntColony/Program.cs

+ 32 - 25
AntColony/Algorithm/ACO.cs

@@ -11,18 +11,15 @@ public class AntColonyOptimizer
     private double[][] distances;
     private double[][] pheromones;
     private readonly double evaporationRate;
-    private readonly int numberOfAnts;
 
     private readonly List<IAnt> xants = [];
-    private IAnt[] ants;
+    private IAnt[]? ants;
 
     /// <summary>
     /// Инициализировать алгоритм
     /// </summary>
     /// <param name="distances">Матрица расстояний</param>
-    /// <param name="numberOfAnts">Количество муравьев</param>
     /// <param name="evaporationRate">Скорость испарения</param>
-    /// <param name="Q">Значение Q</param>
     public AntColonyOptimizer(double[][] distances, double evaporationRate = 0.1)
     {
         numberOfCities = distances.GetLength(0);
@@ -43,20 +40,32 @@ public class AntColonyOptimizer
         }
     }
 
-    public AntColonyOptimizer AddAnts<T>(int count) where T : IAnt
+    /// <summary>
+    /// Добавить определенное количество муравьев заданного типа
+    /// </summary>
+    /// <typeparam name="T">Тип муравьев</typeparam>
+    /// <param name="count">Количество муравьев</param>
+    /// <param name="args">Аргументы конструктора муравья</param>
+    public AntColonyOptimizer AddAnts<T>(int count, params object[] args) where T : IAnt
     {
         for (int i = 0; i < count; i++)
         {
-            xants.Add(Activator.CreateInstance<T>()!);
+            // Создаем нового муравья заданного типа
+            xants.Add((T)Activator.CreateInstance(typeof(T), args)!);
         }
 
         return this;
     }
 
-    public AntColonyOptimizer ShuffleAnts()
+    /// <summary>
+    /// Расположить всех муравьев в случайном порядке
+    /// </summary>
+    /// <param name="shuffle">Расположить муравьев в случайном порядке</param>
+    public AntColonyOptimizer CompileAnts(bool shuffle = false)
     {
         ants = [.. xants];
-        Random.Shared.Shuffle(ants);
+        // Если нужно, располагаем муравьев в случайном порядке
+        if (shuffle) Random.Shared.Shuffle(ants);
 
         return this;
     }
@@ -77,60 +86,58 @@ public class AntColonyOptimizer
     /// <returns>Лучший путь и его расстояние</returns>
     public (List<int> bestTour, double bestDistance) Solve(int maxIterations)
     {
-        // for (int iteration = 0; iteration < maxIterations; iteration++)
+        // Если колония муравьев пуста
+        if (ants is null) throw new MissingMemberException("Колония пуста. Забыли вызвать AntColonyOptimizer.CompileAnts()?");
+
+        Console.WriteLine($"Поиск кратчайшего Гамильтонова цикла с {ants.Length} муравьев и {maxIterations} итераций");
+
         for (int i = 0; i < maxIterations; i++)
         {
             ConcurrentBag<List<int>> antTours = [];
             ConcurrentBag<double> antDistances = [];
 
-            Console.Write($"{i}/{maxIterations} - {bestDistanceEver}\r");
-
             // Каждый муравей строит путь
             var iterationTours = ants.AsParallel().Select(ant =>
             {
                 var ct = ant.ConstructTour(ref distances, ref pheromones);
+                // Если муравей зашел в тупик - убиваем его
                 if (!ct.HasValue) return ([], double.MaxValue);
                 var (tour, distance) = ct.Value;
                 antTours.Add(tour);
                 antDistances.Add(distance);
-                // Console.Write($"\r|- {i}/{numberOfAnts} - {distance}");
 
                 return (tour, distance);
             });
 
+            // После того, как все муравьи построили маршруты, они последовательно (не параллельно) обновляют феромоны
             for (int j = 0; j < antTours.Count; j++)
             {
                 ants[j].UpdatePheromones(ref pheromones);
             }
 
-            // Console.Write(new string(' ', 20));
-
-            // Console.CursorTop--;
-            // Console.CursorLeft = 0;
-
-            // Обновляем феромоны для всех путей
+            // Испаряем феромоны после итерации
             EvaporatePheromones();
-            // File.AppendAllText("./iteration-results", bestDistanceEver.ToString() + '\n');
-            // }
 
+            // Находим лучший путь
             (List<int> Tour, double Distance) bestIterationTour = iterationTours.MinBy(x => x.distance);
-            if (bestIterationTour.Distance < bestDistanceEver)
+            if (bestIterationTour.Distance < bestDistanceEver)  // Если лучше текущего - обновляем текущий
             {
                 bestDistanceEver = bestIterationTour.Distance;
                 bestTourEver = bestIterationTour.Tour;
-                Console.WriteLine();
+                Console.WriteLine($"{i} - {bestDistanceEver,20}");
             }
-        };
+
+            Console.Write($"{i}/{maxIterations} - {bestDistanceEver}\r");
+        }
 
         return (bestTourEver, bestDistanceEver);
     }
 
     /// <summary>
-    /// Обновить матрицу феромонов
+    /// Испарить феромоны
     /// </summary>
     private void EvaporatePheromones()
     {
-        // Испарение
         for (int i = 0; i < numberOfCities; i++)
         {
             for (int j = 0; j < numberOfCities; j++)

+ 10 - 6
AntColony/Algorithm/DefaultAnt.cs

@@ -1,13 +1,14 @@
 
 namespace AntColony.Algorithm;
 
-/// <inheritdoc/>
-public class DefaultAnt : IAnt
+/// <summary>
+/// Стандартная реализация муравья
+/// </summary>
+/// <param name="alpha">Значение α (определяет вес феромонов)</param>
+/// <param name="beta">Значение β (определяет вес расстояния)</param>
+/// <param name="Q">Значение Q (множитель депозита феромонов)</param>
+public class DefaultAnt(double alpha = 1, double beta = 1, double Q = 100) : IAnt
 {
-    private readonly double alpha = 1;
-    private readonly double beta = 1;
-    private readonly double Q = 100;
-
     private List<int> tour = [];
     private double distance = double.PositiveInfinity;
 
@@ -22,16 +23,19 @@ public class DefaultAnt : IAnt
         tour.Add(curCity);
         visited[curCity] = true;
 
+        // Пока остались непосещенные города
         while (tour.Count < distances.Length)
         {
             var sel = SelectNextCity(curCity, ref visited, ref distances, ref pheromones);
             while (!sel.HasValue) sel = SelectNextCity(curCity, ref visited, ref distances, ref pheromones);
+            // Если не можем найти следующий город - самоубиваемся
             if (sel is not { } nextCity) return null;
             tour.Add(nextCity);
             visited[nextCity] = true;
             curCity = nextCity;
         }
 
+        // Если не можем найти путь до стартовой точки - самоубиваемся
         if (distances[curCity][tour[0]] == 0) return null;
 
         tour.Add(tour[0]);

+ 19 - 14
AntColony/Program.cs

@@ -11,12 +11,8 @@ class Program
     /// <param name="json">Файл представлен в формате JSON вместо TSV</param>
     /// <param name="hasHeader">Файл содержит заголовок</param>
     /// <param name="iterations">Количество итераций</param>
-    /// <param name="ants">Количество муравьев</param>
-    /// <param name="alpha">Значение α (определяет вес феромонов)</param>
-    /// <param name="beta">Значение β (определяет вес расстояния)</param>
     /// <param name="evaporationRate">Скорость испарения феромонов</param>
-    /// <param name="q">Значение Q</param>
-    public static void Main(FileInfo file, bool json = false, bool hasHeader = false, int iterations = 100, int ants = 20, double alpha = 1, double beta = 2, double evaporationRate = 0.1, double q = 100)
+    public static void Main(FileInfo file, bool json = false, bool hasHeader = false, int iterations = 100, double evaporationRate = 0.1)
     {
         Console.Write("Чтение файла...");
 
@@ -27,21 +23,30 @@ class Program
 
         Console.WriteLine($"\rСчитано {distances.Length} вершин за {sw.Elapsed} ({sw.ElapsedMilliseconds} мс).\n");
 
-        var aco = new AntColonyOptimizer(
-            distances: distances,
-            evaporationRate: evaporationRate
-        ).AddAnts<DefaultAnt>(1500).ShuffleAnts();
+        var aco = new AntColonyOptimizer(distances: distances, evaporationRate: evaporationRate)
+            .AddAnts<DefaultAnt>(10, 1, 2, 100)  // 10 муравьев, для которых расстояние в 2 раза важнее феромонов
+            .AddAnts<DefaultAnt>(10, 1, 1, 100)  // 10 муравьев, для которых расстояние столь же важно, сколько феромоны
+            .AddAnts<DefaultAnt>(10, 1, 5, 200)  // 10 муравьев, для которых расстояние в 5 раз важнее феромонов, и которые оставляют в 2 раза больше феромонов
+        .CompileAnts(shuffle: true);  // перемешиваем муравьев. По надобности можно оставить в исходном порядке
+
+        sw.Reset();
 
         Console.CancelKeyPress += (e, f) =>
         {
-            var res = aco.EarlyExit();
-            Console.WriteLine($"\nРанняя остановка.\nЛучший путь: {string.Join(" -> ", res.tour)}\nДлина: {res.distance}");
+            var (tour, distance) = aco.EarlyExit();
+            Console.WriteLine("\n\n" + new string('-', Console.BufferWidth));
+            Console.CursorLeft = 0;
+            Console.WriteLine($"\nРанняя остановка. Время работы: {sw.Elapsed}\n");
+            if (tour.Count == 0) Console.WriteLine("Путь не найден.");
+            else Console.WriteLine($"Лучший путь (длина {distance}):\n[ {string.Join(", ", tour)} ]");
         };
 
-        sw.Restart();
+        sw.Start();
 
         var (bestTour, bestDistance) = aco.Solve(iterations);
 
+        Console.WriteLine(new string('-', Console.BufferWidth));
+
         Console.WriteLine($"Задача решена за {sw.Elapsed} ({sw.ElapsedMilliseconds} мс).");
 
         if (bestTour.Count == 0)
@@ -50,8 +55,8 @@ class Program
             return;
         }
 
-        Console.WriteLine($"Лучший путь: {string.Join(" -> ", bestTour)}.");
-        Console.WriteLine($"Итоговое расстояние: {bestDistance}.");
+        Console.WriteLine($"Лучший путь:\n[ {string.Join(", ", bestTour)} ]");
+        Console.WriteLine($"Длина: {bestDistance}.");
     }
 
     private static async Task<double[][]?> ParseEdgeJsonAsync(string filePath)