Browse Source

Ant-Colony Optimization

1ffy 5 months ago
parent
commit
546332be78
6 changed files with 610 additions and 0 deletions
  1. 333 0
      ant-colony/Cargo.lock
  2. 10 0
      ant-colony/Cargo.toml
  3. 34 0
      ant-colony/README.md
  4. 1 0
      ant-colony/example.cmd
  5. 2 0
      ant-colony/example.sh
  6. 230 0
      ant-colony/src/main.rs

+ 333 - 0
ant-colony/Cargo.lock

@@ -0,0 +1,333 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "ant-colony"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "rand",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.5.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "libc"
+version = "0.2.161"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]

+ 10 - 0
ant-colony/Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "ant-colony"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+clap = { version = "4.5.20", features = ["derive"] }
+rand = "0.8.5"

+ 34 - 0
ant-colony/README.md

@@ -0,0 +1,34 @@
+# Муравьиный алгоритм для решения задачи коммивояжера
+## Пример
+![Пример графа](http://media.geeksforgeeks.org/wp-content/uploads/Euler12-300x225.png)
+
+Команда запуска с cargo: `cargo run -q -- -a=1 -b=2 -e=0.1 -i=100 -n=20 -q=100 1-2=10 1-3=15 1-4=20 2-4=25 2-3=35 3-4=30`
+
+Команда запуска исходника: `ant-colony.exe -a=1 -b=2 -e=0.1 -i=100 -n=20 -q=100 1-2=10 1-3=15 1-4=20 2-4=25 2-3=35 3-4=30`
+
+Пример вывода:
+```
+Best path: 1 -> 2 -> 4 -> 3 -> 1
+1-2=10	(pheromone:	307.674). 	Cumulative length:	10
+2-4=25	(pheromone:	307.668). 	Cumulative length:	35
+4-3=30	(pheromone:	307.681). 	Cumulative length:	65
+3-1=15	(pheromone:	0.006).   	Cumulative length:	80
+Total path length:   	.    	.    	.    	.    	.    	80
+```
+
+## Аргументы запуска
+`cargo run -- --help` / `ant-colony.exe --help`
+1. Alpha (double): `--alpha`, `-a`
+2. Beta (double): `--beta`, `-b`
+3. Evaporation (double): `--evaporation`, `-e`
+4. Iterations (usize): `--iterations`, `-i`
+5. Ant count (usize): `--ants`, `-n`
+6. Q value (double): `-q`
+7. Список ребер в формате `A-B=расстояние`
+_!! Стартовой точкой считается самая первая вершина в списке ребер (в примере - 1)_
+
+Пример: `ant-colony.exe -a=1 -b=2 -e=0.1 -i=100 -n=20 -q=100 1-2=10 1-3=15 1-4=20 2-4=25 2-3=35 3-4=30`
+
+## Запуск
+### При наличии cargo (комплекта сборки для Rust):
+`cargo run -- <параметры> <ребра>`

+ 1 - 0
ant-colony/example.cmd

@@ -0,0 +1 @@
+cargo run -q -- -a=1 -b=2 -e=0.1 -i=100 -n=20 -q=100 1-2=10 1-3=15 1-4=20 2-4=25 2-3=35 3-4=30

+ 2 - 0
ant-colony/example.sh

@@ -0,0 +1,2 @@
+#! /bin/bash
+cargo run -q -- -a=1 -b=2 -e=0.1 -i=100 -n=20 -q=100 1-2=10 1-3=15 1-4=20 2-4=25 2-3=35 3-4=30

+ 230 - 0
ant-colony/src/main.rs

@@ -0,0 +1,230 @@
+use std::collections::HashSet;
+use rand::Rng;
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(about, long_about=None)]
+struct Args{
+    #[arg(short='n', long="ants")]
+    num_ants: usize,
+
+    #[arg(short, long)]
+    iterations: usize,
+
+    #[arg(short, long)]
+    alpha: f64,
+
+    #[arg(short, long)]
+    beta: f64,
+
+    #[arg(short, long)]
+    evaporation: f64,
+
+    #[arg(short, long)]
+    q_value: f64,
+
+    #[clap()]
+    edges: Vec<String>,
+}
+
+#[derive(Debug)]
+struct Edge {
+    from: String,
+    to: String,
+    distance: f64,
+    pheromone: f64,
+}
+
+fn parse_edges(edge_strs: &[String]) -> Vec<Edge> {
+    let mut edges = Vec::new();
+    for edge_str in edge_strs {
+        let parts: Vec<&str> = edge_str.split(['=', '-']).collect();
+        if parts.len() != 3 {
+            panic!("Invalid edge format: {}", edge_str);
+        }
+        
+        let distance = parts[2].parse::<f64>()
+            .expect("Distance must be a number");
+            
+        edges.push(Edge {
+            from: parts[0].to_string(),
+            to: parts[1].to_string(),
+            distance,
+            pheromone: 1.0,
+        });
+        
+        edges.push(Edge {
+            from: parts[1].to_string(),
+            to: parts[0].to_string(),
+            distance,
+            pheromone: 1.0,
+        });
+    }
+    edges
+}
+
+fn construct_solution(
+    edges: &[Edge],
+    vertices: &Vec<String>,
+    alpha: f64,
+    beta: f64,
+) -> Vec<String> {
+    let mut rng = rand::thread_rng();
+    // Текущая вершина (стартовая)
+    let mut current = vertices.iter().nth(0).unwrap().clone();
+    // Текущий построенный путь
+    let mut path = vec![current.clone()];
+    // Список непосещенных вершин
+    let mut unvisited: HashSet<_> = vertices.iter().cloned().filter(|v| v != &current).collect();
+
+    // Пока существуют непосещенные вершины
+    while !unvisited.is_empty() {
+        // Список доступных ребер, на которые мы можем пойти из текущей вершины
+        let available_edges: Vec<_> = edges.iter()
+            .filter(|e| e.from == current && unvisited.contains(&e.to))
+            .collect();
+
+        // Если нет доступных ребер
+        if available_edges.is_empty() {
+            break;
+        }
+
+        // Сумма ощущаемых феромонов
+        let total: f64 = available_edges.iter()
+            .map(|e| (e.pheromone.powf(alpha)) * ((1.0 / e.distance).powf(beta)))
+            .sum();
+
+        // Вероятность пойти на каждое ребро
+        let probabilities: Vec<f64> = available_edges.iter()
+            .map(|e| (e.pheromone.powf(alpha)) * ((1.0 / e.distance).powf(beta)) / total)
+            .collect();
+
+        let mut cumsum = 0.0;
+        let rand_val = rng.gen::<f64>();
+        // Выбираем следующее ребро
+        let selected = available_edges.iter().zip(probabilities.iter())
+            .find(|(_, &p)| {
+                cumsum += p;
+                rand_val <= cumsum
+            })
+            .map(|(e, _)| e)
+            .unwrap();
+
+        // Переходим на следующую вершину
+        current = selected.to.clone();
+        // Сохраняем новую текущую вершину в путь
+        path.push(current.clone());
+        // Удаляем посещенную вершину из списка непосещенных
+        unvisited.remove(&current);
+    }
+
+    path
+}
+
+fn calculate_path_length(path: &[String], edges: &[Edge]) -> f64 {
+    // Считаем длину пути
+    path.windows(2)
+        .map(|window| {
+            let from = &window[0];
+            let to = &window[1];
+            edges.iter()
+                .find(|e| e.from == *from && e.to == *to)
+                .map(|e| e.distance)
+                .unwrap()
+        })
+        .sum()
+}
+
+fn update_pheromones(
+    edges: &mut [Edge],
+    solutions: &[(Vec<String>, f64)],
+    evaporation: f64, 
+    q_value: f64,
+) {
+    // Испаряем часть феромонов со всех рёбер графа
+    for edge in edges.iter_mut() {
+        edge.pheromone *= 1.0 - evaporation;
+    }
+
+    // Для каждого найденного пути добавляем новые феромоны
+    for (path, length) in solutions {
+        // Вычисляем количество феромонов для отложения
+        // Чем короче путь, тем больше феромонов откладывается
+        let deposit = q_value / length;
+
+        // Проходим по всем парам последовательных вершин в пути
+        path.windows(2).for_each(|window| {
+            let from = &window[0];
+            let to = &window[1];
+
+            // Добавляем феромоны на прямое ребро (from -> to)
+            if let Some(edge) = edges.iter_mut().find(|e| e.from == *from && e.to == *to) {
+                edge.pheromone += deposit;
+            }
+            // Добавляем феромоны на обратное ребро (to -> from), 
+            // так как граф неориентированный
+            if let Some(edge) = edges.iter_mut().find(|e| e.from == *to && e.to == *from) {
+                edge.pheromone += deposit;
+            }
+        });
+    }
+}
+
+fn main() {
+    // Парсим аргументы командной строки
+    let opt = Args::parse();
+    let mut edges = parse_edges(&opt.edges);
+    
+    // Собираем все вершины
+    let vertices: Vec<String> = edges.iter()
+        .flat_map(|e| vec![e.from.clone(), e.to.clone()])
+        .collect();
+
+    let mut best_solution = None;
+    let mut best_length = f64::INFINITY;
+
+    // Основной цикл итераций
+    for _ in 0..opt.iterations {
+        let mut solutions = Vec::new();
+
+        for _ in 0..opt.num_ants {
+            // Строим путь
+            let path = construct_solution(&edges, &vertices, opt.alpha, opt.beta);
+            // Находим длину пути
+            let length = calculate_path_length(&path, &edges);
+            // Сохраняем решение
+            solutions.push((path.clone(), length));
+
+            if length < best_length {
+                best_length = length;
+                best_solution = Some(path);
+            }
+        }
+
+        update_pheromones(&mut edges, &solutions, opt.evaporation, opt.q_value);
+    }
+
+    // Если решение найдено
+    if let Some(_path) = best_solution {
+        let mut path = _path.clone();
+        // Добавляем путь до стартовой вершины
+        path.push(vertices.iter().nth(0).clone().unwrap().to_string());
+        best_length += edges.iter().filter(|e| e.from == path.first().unwrap().to_string() && e.to == _path.last().unwrap().to_string()).nth(0).unwrap().distance;
+        println!("Best path: {}", path.join(" -> "));
+
+        let mut sum = 0f64;
+
+        // Считаем сумму
+        for edgenum in 1..path.len(){
+            let prev = path[edgenum - 1].clone();
+            let cver = path[edgenum].clone();
+            let edge = &edges.iter().filter(|e| e.from == prev && e.to == cver).last().expect("");
+            sum += edge.distance;
+            println!("{}-{}={}\t(pheromone:\t{:.3}). \tCumulative length:\t{}", edge.from, edge.to, edge.distance, edge.pheromone, sum);
+        }
+
+        println!("Total path length:\t.\t.\t.\t.\t.\t{}", best_length);
+    } else {
+        println!("No solution found.");
+    }
+}