非对称TSP问题的禁忌搜索算法求解的Java实现

注:本文章基于学校优化计算方法选修课程的实验内容,可供研究相关领域的初学者作为入门学习以及编程实现的案例参照。

一. 非对称TSP问题:

        旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。(定义摘自百度百科~)

        非对称TSP问题的特殊之处在于两个城市的往与返成本是不同的,因为本文章所采用的禁忌搜索算法在对称与非对称的两种问题求解实现上差别不大,因此这里不做深入探究。

二. 禁忌搜索算法:

        这里仅列出禁忌搜索算法设计的关键步骤,感兴趣的同学可以查询相书籍或博客以做深入了解。

        ①已知当前解以及当前解的函数值。

        ②创建可进行邻域移动的解集、初始化禁忌表。

        ③判断当前解集中的最优移动是否在禁忌表中。若不在禁忌表中则将此移动作为当前解;若在禁忌表中,则判断此移动能否打破禁忌(在TSP中则将当前解的函数值由于历史最优解的函数值视为打破禁忌),若不能打破禁忌,则舍弃此移动方式,继续判断次优移动是否满足条件,依此类推,直到找到满足条件的邻域移动方式。

        ④更新当前函数值f,历最优值A。

        ⑤判断是否达到最大迭代次数,达到则结束算法,否则回到步骤②。

三. 编程实现

1. 编程环境:

        Java1.8、maven

2. 数据:

        实验课提供的矩阵。

3. 代码奉上:

        ① 定义Ts算法的主体实现类:

public class Ts {
    private int A, f;                       //历史最优A,当前目标函数值f
    private int gen, T_size, cnum;          // 迭代次数gen,禁忌表长度T_size,城市数量cnum
    private ArrayList<MoveNeigbor> tab = new ArrayList<>();             // tab是邻域表,用顺序表实现
    private LimitedQueue<MoveNeigbor> T;     // T是禁忌表,用固定长度队列实现
    private Route s_;                        //最优路径s_
    private Route s;                         //当前路径s
    private String csvPath = "/Users/Henry/Desktop/task/data/data.csv";
    private int[][] dis= new int[this.cnum][this.cnum];
    // 迭代求解
    public void iterate(){
        initial();
        // 获取所有可行的邻域移动方式
        getTab();
        for (int i = 0; i < this.gen; i++) {
            // 第一步先排序
            this.tab.sort(new Comparator<MoveNeigbor>() {
                // 重写比较器的比较方法
                @Override
                public int compare(MoveNeigbor o1, MoveNeigbor o2) {
                    return get_f(o1) - get_f(o2);
                }
            });
            // 第二步循环,从头到尾看tab中的邻域移动方式是否在禁忌表中,
            // 若该移动方式在禁忌表中,则判断该方式是否能打破禁忌(f > A),
            // 若能打破禁忌,则取该种方式;若无法打破禁忌,则继续搜索,直到
            // 搜索到不在禁忌表中的移动方式或是能够打破禁忌的移动方式,采取
            // 该方式,break停止循环,并更新T(禁忌表)以及f(当前最优)。
            for (MoveNeigbor moveNeigbor : this.tab) {
                if (this.T.contains(moveNeigbor)) {
                    if (get_f(moveNeigbor) < this.A) {               
                        this.s.makeAMove(moveNeigbor);
                        // this.T.add(moveNeigbor);
                        this.f = get_f(moveNeigbor);
                        break;
                    }
                } else {
                    this.s.makeAMove(moveNeigbor);
                    this.T.add(moveNeigbor);
                    this.f = get_f(moveNeigbor);
                    break;
                }
            }
            // 第三步,更新s_(最优路径)、更新A(历史最优)
            if (this.f < this.A) {
                this.A = this.f;
                s_ = new Route(this.s.getRt());
            }
            // 打印日志
            if(i % 10 == 0){
                System.out.println("第" + i + "代————" + "历史最优A:" + this.A + " 当前优化f:" + this.f);
            }
        }
    }
    // 构造方法
    public Ts(int t_size, int gen,int cnum) {
        this.gen = gen;
        this.T_size = t_size;
        this.cnum = cnum;
        this.T = new LimitedQueue<>(T_size);
    }
    // 初始化解集状态
    public void initial(){
        ReadCsv cr = new ReadCsv(this.csvPath);
        this.dis = cr.getArray(this.cnum);
        this.s = setRandS();
        this.s_ = new Route(this.s.getRt());
        this.f = getF(this.s.getRt());
        this.A = this.f;
    }
    // setS方法用于生成随机顺序的初始解
    public Route setRandS(){
        ArrayList<Double> arr1 = new ArrayList<>();
        ArrayList<Double> arr2 = new ArrayList<>();
        ArrayList<Integer> arr = new ArrayList<>();
        // 初始化arr
        for (int i = 0; i < this.cnum; i++) {
            arr.add(i);
        }
        // 随机初始化arr1
        for (int i = 0; i < this.cnum; i++) {
            arr1.add(Math.random());
        }
        // copy arr1给arr2
        for (int i = 0; i < this.cnum; i++) {
            arr2.add(arr1.get(i));
        }
        arr2.sort(Comparator.naturalOrder());
        for (int i = 0; i < this.cnum; i++) {
            int idx = arr2.indexOf(arr1.get(i));
            arr.set(idx, i);
        }
        return new Route(arr);
    }
    // 创建方法获取某一状态的目标函数值
    public int getF(ArrayList<Integer> rt_){
        int sum = 0;
        for (int i = 0; i < this.cnum; i++) {
            sum = sum + this.dis[rt_.get(i)][rt_.get((i + 1) % this.cnum)];
        }
        return sum;
    }
    // 定义评估当前路径进行进行某种路径移动后的目标函数值的方法
    public int get_f(MoveNeigbor m){
        ArrayList<Integer> rt_ = (ArrayList<Integer>) this.s.getRt().clone();
        ArrayList<Integer> loc = m.getLoc();
        int idx1 = rt_.indexOf(loc.get(0));
        int idx2 = rt_.indexOf(loc.get(1));
        Integer tempVar = rt_.get(idx1);
        rt_.set(idx1, rt_.get(idx2));
        rt_.set(idx2, tempVar);
        return getF(rt_);
    }
    // 得到当前解的所有邻域
    public void getTab(){
        for (int i = 0; i < this.cnum; i++) {
            for (int j = i + 1; j < this.cnum; j++) {
                this.tab.add(new MoveNeigbor(i, j));
            }
        }
    }
}

       ②定义描述邻域移动行为的类,这里使用2-opt领域移动方式:

class MoveNeigbor {
    // movm代表交换i和j的动作
    public HashSet<Integer> movm = new HashSet<>();
    // 通过构造方法设置movm属性
    public MoveNeigbor(Integer i, Integer j) {
        this.movm.add(i);
        this.movm.add(j);
    }
    // 重写equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MoveNeigbor that = (MoveNeigbor) o;
        return that.movm.equals(this.movm);
    }

    // 定义方法将movm转为顺序表并返回
    public ArrayList<Integer> getLoc() {
        Iterator it = this.movm.iterator();
        Integer loc1 = (Integer) it.next();
        Integer loc2 = (Integer) it.next();
        ArrayList<Integer> loc = new ArrayList<>();
        loc.add(loc1);
        loc.add(loc2);
        return loc;
    }
}

        ③定义描述当前路径的类:

class Route {
    // 属性rt描述当前路径
    private ArrayList<Integer> rt = new ArrayList<>();

    public ArrayList<Integer> getRt() {
        return (ArrayList<Integer>) rt.clone();
    }
    // 构造方法
    public Route(ArrayList<Integer> rt) {
        this.rt = (ArrayList<Integer>) rt.clone();
    }
    // 定义对当前路径进行邻域移动的方法
    public void makeAMove(MoveNeigbor m){
        ArrayList<Integer> loc = m.getLoc();
        int idx1 = rt.indexOf(loc.get(0));
        int idx2 = rt.indexOf(loc.get(1));
        Integer tempVar = this.rt.get(idx1);
        this.rt.set(idx1, this.rt.get(idx2));
        this.rt.set(idx2, tempVar);
    }
    // 定义获取指定索引值的方法
    public Integer get(int i){
        return this.rt.get(i);
    }
}

       ④固定长度队列数据结构的实现:

class LimitedQueue<E> extends LinkedList<E> {
    private static final long serialVersionUID = 1L;
    private final int size;

    public LimitedQueue(int size) {
        this.size = size;
    }
    @Override
    public boolean add(E o) {
        super.add(o);
        while (size() > size) {
            super.remove();
        }
        return true;
    }
}

        ⑤定义csv文件读取器,需要用到opencsv:

class ReadCsv {
    // 定义构造方法,传入文件路径
    private String path;
    public ReadCsv(String path) {
        this.path = path;
    }
    // 定义方法得到文件解析后的数组
    public int[][] getArray(int y){
        int[][] arr = new int[y][y];
        CSVReader reader = null;
        int x = 0;
        try {
            reader = new CSVReader(new FileReader(path));
            String[] line;
            while ((line = reader.readNext()) != null) {
                for (int i = 0; i < y; i++) {
                    arr[x][i] = Integer.parseInt(line[i]);
                }
                x++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return arr;
    }
}

4. 运行效果:

        

 四. 后记

        博客是心血来潮写的,代码能力是半吊子的。希望能够为翻到这篇博客的有缘人产生价值。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TSP问题是指旅行问题,即在一个图中,找到一条路径,经过每个顶点恰好一次,最终回到起点,并且路径总长度最短。TSP问题是一个NP难问题,没有多项式时间内的精确解法,因此我们通常采用一些近似算法来求解。其中,常用的有贪心算法和近似算法。 下面让我们来介绍一下java实现TSP问题的近似算法求解。 1. 贪心算法 贪心算法是一种启发式算法,它不一定能得到最优解,但通常能得到较优的解。TSP问题的贪心算法思路如下: (1)任选一个起点,并将它作为当前节点; (2)从当前节点出发,选择一条最短的边,到达下一个节点; (3)重复步骤(2),直到所有的节点都被访问过; (4)返回起点,形成一个回路。 Java代码实现如下: ```java public class TSP { private int[][] graph; // 图 private boolean[] visited; // 是否已经访问 private int[] path; // 访问路径 private int minDist; // 最短距离 private int n; // 节点个数 public TSP(int[][] graph) { this.graph = graph; this.visited = new boolean[graph.length]; this.path = new int[graph.length]; this.minDist = Integer.MAX_VALUE; this.n = graph.length; } // 从当前节点出发,选择一条最短的边,到达下一个节点 private void dfs(int cur, int dist, int level) { if (level == n) { // 所有节点都被访问过,形成一个回路 if (graph[cur][0] != 0) { dist += graph[cur][0]; if (dist < minDist) { minDist = dist; System.arraycopy(path, 0, bestPath, 0, n); } } return; } for (int i = 0; i < n; i++) { if (!visited[i] && graph[cur][i] != 0) { visited[i] = true; path[level] = i; dfs(i, dist+graph[cur][i], level+1); visited[i] = false; path[level] = -1; } } } // 求解TSP问题 public void solve() { visited[0] = true; path[0] = 0; dfs(0, 0, 1); } public int getMinDist() { return minDist; } public int[] getBestPath() { return bestPath; } } ``` 2. 近似算法 近似算法是一种能够在多项式时间内得到较优解的算法。其中,最常用的近似算法是 Christofides算法。它的思路是: (1)通过最小生成树来构造一条欧拉回路; (2)将欧拉回路转化为哈密顿回路。 Java代码实现如下: ```java public class TSP { private int[][] graph; // 图 private int[] path; // 访问路径 private int minDist; // 最短距离 private int n; // 节点个数 public TSP(int[][] graph) { this.graph = graph; this.path = new int[graph.length]; this.minDist = Integer.MAX_VALUE; this.n = graph.length; } // 求解TSP问题 public void solve() { // Step 1: 通过最小生成树来构造一条欧拉回路 Prim prim = new Prim(graph); int[][] mst = prim.getMST(); int[] degree = new int[n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (mst[i][j] != 0) { degree[i]++; } } } int oddCnt = 0, oddVertex = -1; for (int i = 0; i < n; i++) { if (degree[i] % 2 == 1) { oddCnt++; oddVertex = i; } } if (oddCnt > 2) { throw new IllegalArgumentException("无欧拉回路"); } if (oddCnt == 2) { // 添加一条边,使得变成欧拉回路 for (int i = 0; i < n; i++) { if (degree[i] % 2 == 1) { for (int j = 0; j < n; j++) { if (degree[j] % 2 == 1 && i != j) { if (graph[i][j] != 0 && (oddVertex == -1 || graph[i][j] < graph[i][oddVertex])) { oddVertex = j; } } } } } mst[oddVertex][oddVertex] = Integer.MAX_VALUE; } int[] eulerPath = new EulerPath(mst).getPath(); // Step 2: 将欧拉回路转化为哈密顿回路 boolean[] visited = new boolean[n]; int level = 0; for (int i = 0; i < eulerPath.length; i++) { int cur = eulerPath[i]; if (!visited[cur]) { visited[cur] = true; path[level++] = cur; } } if (level != n) { throw new IllegalArgumentException("无哈密顿回路"); } path[level] = path[0]; int dist = 0; for (int i = 0; i < n; i++) { dist += graph[path[i]][path[i+1]]; } minDist = dist; } public int getMinDist() { return minDist; } public int[] getBestPath() { return path; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值