zoukankan      html  css  js  c++  java
  • 【算法】A*改进算法

    目的:我这里希望实现一个java A* 游戏里的战斗寻径

    定义部分: 这个定义引用自 http://www.cnblogs.com/kanego/archive/2011/08/30/2159070.html

    这个伪代码说的很详细

    如下的状态空间:(起始位置是A,目标位置是P,字母后的数字表示节点的估价值) 

        搜索过程中设置两个表:OPEN和CLOSED。OPEN表保存了所有已生成而未考察的节点,CLOSED 表中记录已访问过的节点。算法中有一步是根据估价函数重排OPEN表。这样循环中的每一 步只考虑OPEN表中状态最好的节点。具体搜索过程如下: 

        1)初始状态: 
        OPEN=[A5];CLOSED=[]; 
        2)估算A5,取得搜有子节点,并放入OPEN表中; 
        OPEN=[B4,C4,D6];CLOSED=[A5] 
        3)估算B4,取得搜有子节点,并放入OPEN表中; 
        OPEN=[C4,E5,F5,D6];CLOSED=[B4,A5] 
        4)估算C4;取得搜有子节点,并放入OPEN表中; 
        OPEN=[H3,G4,E5,F5,D6];CLOSED=[C4,B4,A5] 
        5)估算H3,取得搜有子节点,并放入OPEN表中; 
        OPEN=[O2,P3,G4,E5,F5,D6];CLOSED=H3C4,B4,A5] 
        6)估算O2,取得搜有子节点,并放入OPEN表中; 
        OPEN=[P3,G4,E5,F5,D6];CLOSED=[O2,H3,C4,B4,A5] 
        7)估算P3,已得到解; 
        看了具体的过程,再看看伪程序吧。算法的伪程序如下: 

     1 关于A*算法 伪代码
     2 Best_First_Search()
     3 {
     4  Open   =   [起始节点];
     5  Closed   =   [];
     6  while   (Open表非空)
     7  {
     8   从Open中取得一个节点X,并从OPEN表中删除。
     9   if   (X是目标节点)
    10   {
    11    求得路径PATH;
    12    返回路径PATH;
    13   }
    14   for   (每一个X的子节点Y)
    15   {
    16    if   (Y不在OPEN表和CLOSE表中)
    17    {
    18     求Y的估价值;
    19     并将Y插入OPEN表中;
    20    }
    21    //还没有排序
    22    else   if   (Y在OPEN表中)
    23    {
    24     if   (Y的估价值小于OPEN表的估价值)
    25      更新OPEN表中的估价值;
    26    }
    27    else   //Y在CLOSE表中
    28    {
    29     if   (Y的估价值小于CLOSE表的估价值)
    30     {
    31      更新CLOSE表中的估价值;
    32      从CLOSE表中移出节点,并放入OPEN表中;
    33     }
    34    }
    35    将X节点插入CLOSE表中;
    36    按照估价值将OPEN表中的节点排序;
    37   }//end   for
    38  }//end   while
    39 }//end   func 

    看看java的A*改进在游戏的战斗里怎么搞

      1 package ...combat.service.impl;
      2 
      3 /*    
      4  * A* algorithm implementation.
      5  * Copyright (C) 2007, 2009 Giuseppe Scrivano <gscrivano@gnu.org>
      6 
      7  * This program is free software; you can redistribute it and/or modify
      8  * it under the terms of the GNU General Public License as published by
      9  * the Free Software Foundation; either version 3 of the License, or
     10  * (at your option) any later version.
     11 
     12  * This program is distributed in the hope that it will be useful,
     13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15  * GNU General Public License for more details.
     16 
     17  * You should have received a copy of the GNU General Public License along
     18  * with this program; if not, see <http://www.gnu.org/licenses/>.
     19  */
     20 
     21 import java.util.HashMap;
     22 import java.util.LinkedList;
     23 import java.util.List;
     24 import java.util.PriorityQueue;
     25 
     26 import org.slf4j.Logger;
     27 import org.slf4j.LoggerFactory;
     28 
     29 /*
     30  * 网上找的AStar寻路算法改编版
     31  * 
     32  * 地图的Y轴必须在100以内,point的hashCode
     33  */
     34 public class PathFinder {
     35     protected static final Logger log = LoggerFactory
     36             .getLogger(PathFinder.class);
     37     private static final int MAXLOOPCOUNT = 4000;
     38 
     39     public static final byte MAP_FLAG_NONE = 0;//可以走
     40     public static final byte MAP_FLAG_ONE = 1;//攻击方区域
     41     public static final byte MAP_FLAG_TWO = 2;//防守方区域
     42     public static final byte MAP_FLAG_THREE = 3;//不可走的障碍区域
     43     /**
     44      * 0表示没有障碍的点 , 1,2都是障碍点,行走者自身包含1或者2的属性,与自身属性相同的表示可穿过不可停留的点 ,不同表示不可穿越不可停留的点
     45      * 3表示不可移动到的点
     46      */
     47     private byte[][] map;//地图
     48 
     49     private Node goal;//目标点
     50     private PriorityQueue<Path> paths;//路径优先级队列
     51     private HashMap<Integer, Integer> mindists;
     52     /*
     53      * 目标范围,表示距离目标多少个格子就算到达目标
     54      */
     55     private int goalRange;
     56 
     57     //private int maxStep;// 移动的最大距离
     58 
     59     private byte mapFlag;// 1或者2
     60     // private double lastCost;
     61     private int expandedCounter;
     62 
     63     private int xstep = 1;
     64     private int ystep = 1;
     65 
     66     public PathFinder(byte[][] map) {
     67         this.map = map;
     68         paths = new PriorityQueue<Path>();
     69         mindists = new HashMap<Integer, Integer>();
     70         expandedCounter = 0;
     71     }
     72     //goalRange 攻击范围   maxStep移动范围   mapFlag本方属性1or2
     73     public List<Node> getAtkPath(Node start, Node goal, int goalRange,
     74             int maxStep, byte mapFlag) {
     75         Path path = computeAtkPath(start, goal, goalRange, maxStep, mapFlag);
     76         this.clean();
     77         if (path != null) {
     78             return path.getNodes();
     79         } else {
     80             return null;
     81         }
     82     }
     83 
     84     // 这是为城墙单独做的一个方法,主要是目前寻路,不支持一个目标站多个位置
     85     public List<Node> getAtkPath(Node start, List<Node> goals, int goalRange,
     86             int maxStep, byte mapFlag) {
     87         Path bestP = null;
     88         for (Node goal : goals) {
     89             Path path = computeAtkPath(start, goal, goalRange, maxStep, mapFlag);
     90             this.clean();
     91             if (bestP == null) {
     92                 bestP = path;
     93             } else {
     94                 if (path != null && path.depth < bestP.depth) {
     95                     bestP = path;
     96                 }
     97             }
     98         }
     99         if (bestP != null) {
    100             return bestP.getNodes();
    101         } else {
    102             return null;
    103         }
    104 
    105     }
    106 
    107     /**
    108      * 寻找攻击路线
    109      * 
    110      * @param start
    111      * @param goal
    112      * @param goalRange 攻击范围
    113      * @param maxStep 移动力【移动范围】
    114      * @param mapFlag 移动方阵营,如果移动到的位置不为0,但是与本方的阵营相同,可以穿过去
    115      * @return
    116      */
    117     public Path computeAtkPath(Node start, Node goal, int goalRange,
    118             int maxStep, byte mapFlag) {
    119         if (goal != null) {
    120             this.setGoal(goal);
    121         }
    122         if (goalRange > 0) {
    123             this.goalRange = goalRange;
    124         }
    125         // if (maxStep>0){
    126         //this.maxStep = maxStep;
    127         // }
    128         if (mapFlag != MAP_FLAG_ONE && mapFlag != MAP_FLAG_TWO) {
    129             this.mapFlag = MAP_FLAG_ONE;
    130         } else {
    131             this.mapFlag = mapFlag;
    132         }
    133 
    134         if (start.x > goal.x) {
    135             xstep = -1;
    136         } else {
    137             xstep = 1;
    138         }
    139 
    140         if (start.y > goal.y) {
    141             ystep = -1;
    142         } else {
    143             ystep = 1;
    144         }
    145 
    146         try {
    147 
    148             Path root = new Path();
    149             root.setPoint(start);
    150 
    151             if (isGoal(start) && (map[start.x][start.y] != MAP_FLAG_ONE)) {// 不需要移动,当前点就在目标点范围内
    152                 return root;
    153             }
    154             /* Needed if the initial point has a cost. */
    155             f(root, start, start);
    156 
    157             expand(root);
    158 
    159             for (int j = 0; j < MAXLOOPCOUNT; j++) {
    160                 Path p = paths.poll();
    161 
    162                 if (p == null) {
    163                     return null;
    164                 }
    165 
    166                 if (p.f < 0) {// 该路径不可达,必然有敌方阻挡,则有更合适的目标,放弃当前目标
    167                     continue;
    168                 }
    169 
    170                 Node last = p.getPoint();
    171 
    172                 if (isGoal(last)) {
    173                     // 停留点不是友军已经停留位置
    174                     if (map[last.x][last.y] == MAP_FLAG_NONE) {
    175                         return p;
    176                     }
    177                 }
    178                 if (p.depth < maxStep) {// 超过最大步数
    179                     expand(p);
    180                 }
    181 
    182             }
    183         } catch (Exception e) {
    184             e.printStackTrace();
    185         } finally {
    186             // if (log.isDebugEnabled()){
    187             // log.debug("尝试的节点数:{}"+this.getExpandedCounter());
    188             // }
    189         }
    190         return null;
    191 
    192     }
    193 
    194     /**
    195      * 
    196      * 移动目标未必可达,只是找可移动到的离目标最近的点
    197      * 
    198      * @param start
    199      * @param goal
    200      * @param goalRange
    201      *            目标周围goalRange-1格都作为目标
    202      * @param maxStep
    203      *            移动步数超过maxStep的忽略
    204      * @param mapFlag
    205      * @return
    206      */
    207     public List<Node> getMovePath(Node start, Node goal, int goalRange,
    208             int maxStep, byte mapFlag) {
    209         Path path = computeMovePath(start, goal, goalRange, maxStep, mapFlag);
    210         this.clean();
    211         if (path != null) {
    212             return path.getNodes();
    213         } else {
    214             return null;
    215         }
    216     }
    217 
    218     // 这是为城墙单独做的一个方法,主要是目前寻路,不支持一个目标站多个位置
    219     public List<Node> getMovePath(Node start, List<Node> goals, int goalRange,
    220             int maxStep, byte mapFlag) {
    221         Path bestP = null;
    222         for (Node goal : goals) {
    223             Path path = computeMovePath(start, goal, goalRange, maxStep,
    224                     mapFlag);
    225             this.clean();
    226             if (bestP == null) {
    227                 bestP = path;
    228             } else {
    229                 if (path != null && path.depth < bestP.depth) {
    230                     bestP = path;
    231                 }
    232             }
    233         }
    234         if (bestP != null) {
    235             return bestP.getNodes();
    236         } else {
    237             return null;
    238         }
    239 
    240     }
    241 
    242     public Path computeMovePath(Node start, Node goal, int goalRange,
    243             int maxStep, byte mapFlag) {
    244 
    245         Path bestPath = null;// 最佳路径
    246 
    247         if (goal != null) {
    248             this.setGoal(goal);
    249         }
    250         if (goalRange > 0) {
    251             this.goalRange = goalRange;
    252         }
    253         // if (maxStep>0){
    254         // this.maxStep = maxStep;
    255         // }
    256         if (mapFlag != MAP_FLAG_ONE && mapFlag != MAP_FLAG_TWO) {
    257             this.mapFlag = MAP_FLAG_ONE;
    258         } else {
    259             this.mapFlag = mapFlag;
    260         }
    261 
    262         if (start.x > goal.x) {
    263             xstep = -1;
    264         } else {
    265             xstep = 1;
    266         }
    267 
    268         if (start.y > goal.y) {
    269             ystep = -1;
    270         } else {
    271             ystep = 1;
    272         }
    273 
    274         try {
    275 
    276             Path root = new Path();
    277             root.setPoint(start);
    278 
    279             if (isGoal(start) && (map[start.x][start.y] != MAP_FLAG_ONE)) {// 不需要移动,当前点就在目标点范围内
    280                 return root;
    281             }
    282             /* Needed if the initial point has a cost. */
    283             f(root, start, start);
    284 
    285             expand(root);
    286             int j=0;
    287             for (j = 0; j < MAXLOOPCOUNT; j++) {
    288                 Path p = paths.poll();
    289 
    290                 if (p == null) {
    291                     //System.out.println("=========is null=================");
    292                     break;
    293                 }
    294 
    295                 if (p.f < 0) {// 该路径不可达,必然有敌方阻挡,则有更合适的目标,放弃当前目标
    296                     continue;
    297                 }
    298 
    299                 if (p.depth == maxStep) {
    300                     // 如果前maxstep都不能站立,则放弃该路径
    301                     boolean valid = false;
    302                     for (Path i = p; i != null && i.parent != null; i = i.parent) {
    303                         if (i.depth <= maxStep) {
    304                             Node tmp = i.getPoint();
    305                             if (map[tmp.x][tmp.y] == MAP_FLAG_NONE) {
    306                                 valid = true;
    307                                 break;
    308                             }
    309                         }
    310                     }
    311 
    312                     if (!valid) {
    313                         continue;
    314                     }
    315                 }
    316 
    317                 Node last = p.getPoint();
    318                 // 是目标,并且不是友军站住的位置
    319                 if (isGoal(last) && map[last.x][last.y] != MAP_FLAG_ONE) {
    320                     if (map[last.x][last.y] == MAP_FLAG_NONE) {
    321                         for (Path i = p; i != null && i.parent != null; i = i.parent) {
    322                             if (i.depth <= maxStep) {
    323                                 Node tmp = i.getPoint();
    324                                 if (map[tmp.x][tmp.y] == MAP_FLAG_NONE) {
    325                                     return i;// 只返回能走的最大步数
    326                                 }
    327                             }
    328                         }
    329 
    330                     }
    331                 }
    332 
    333                 // 保留不可达路径中与目标点最近的路径
    334                 if (map[last.x][last.y] == MAP_FLAG_NONE) {
    335                     if (bestPath == null) {
    336                         bestPath = p;
    337                     } else if (p.depth <= maxStep) {
    338                         Node bestNode = bestPath.getPoint();
    339                         if (Math.abs(last.x - goal.x)
    340                                 + Math.abs(last.y - goal.y) < Math
    341                                 .abs(bestNode.x - goal.x)
    342                                 + Math.abs(bestNode.y - goal.y)) {
    343                             bestPath = p;
    344                         }
    345                     }
    346                 }
    347                 expand(p);
    348 
    349             }
    350             //System.out.println("=========================="+j);
    351         } catch (Exception e) {
    352             e.printStackTrace();
    353         } finally {
    354             // if (log.isDebugEnabled()){
    355             // log.debug("尝试的节点数:{}"+this.getExpandedCounter());
    356             // }
    357         }
    358 
    359         return bestPath;
    360 
    361     }
    362 
    363     public int getExpandedCounter() {
    364         return expandedCounter;
    365     }
    366 
    367     protected int g(Node from, Node to) {
    368 
    369         if (from.x == to.x && from.y == to.y)
    370             return 0;
    371         if (map[to.x][to.y] == MAP_FLAG_NONE || map[to.x][to.y] == mapFlag)
    372             return 1;
    373         if (isGoal(to))
    374             return 1;
    375 
    376         return Integer.MIN_VALUE;// 敌方阻挡位置,设置为负值,表示不可到达
    377     }
    378 
    379     protected int h(Node from, Node to) {
    380         /* Use the Manhattan distance heuristic. */
    381         // return new Double(Math.abs(map[0].length - 1 - to.x)
    382         // + Math.abs(map.length - 1 - to.y));
    383         return Math.abs(goal.x - to.x) + Math.abs(goal.y - to.y);
    384     }
    385 
    386     protected int f(Path p, Node from, Node to) {
    387         int g = g(from, to) + ((p.parent != null) ? p.parent.g : 0);
    388         int h = h(from, to);
    389 
    390         p.g = g;
    391         p.f = g + h;
    392 
    393         return p.f;
    394     }
    395 
    396     private void expand(Path path) {
    397         Node p = path.getPoint();
    398         Integer min = mindists.get(path.getPoint().hashCode());
    399 
    400         /*
    401          * If a better path passing for this point already exists then don't
    402          * expand it.
    403          */
    404         if (min == null || min.intValue() > path.f)
    405             mindists.put(path.getPoint().hashCode(), path.f);
    406         else
    407             return;
    408 
    409         List<Node> successors = generateSuccessors(p);
    410 
    411         for (Node t : successors) {
    412             if (!mindists.containsKey(t.hashCode())) {
    413                 Path newPath = new Path(path);
    414                 newPath.setPoint(t);
    415                 f(newPath, path.getPoint(), t);
    416                 // System.out.println(newPath.toString());
    417                 paths.offer(newPath);
    418             }
    419         }
    420 
    421         expandedCounter++;
    422     }
    423 
    424     protected Node getGoal() {
    425         return this.goal;
    426     }
    427 
    428     protected void setGoal(Node node) {
    429         this.goal = node;
    430 
    431     }
    432 
    433     protected boolean isGoal(Node node) {
    434         if (goalRange <= 0) {
    435             return (node.x == goal.x) && (node.y == goal.y);
    436         } else {
    437             return (Math.abs(goal.x - node.x) + Math.abs(goal.y - node.y) <= goalRange);
    438         }
    439     }
    440 
    441     protected boolean isGoal(int x, int y) {
    442         if (goalRange <= 0) {
    443             return (x == goal.x) && (y == goal.y);
    444         } else {
    445             return (Math.abs(goal.x - x) + Math.abs(goal.y - y) <= goalRange);
    446         }
    447     }
    448 
    449     protected boolean isBarrier(int x, int y) {
    450         return (map[x][y] != MAP_FLAG_NONE && map[x][y] != mapFlag);
    451     }
    452 
    453     protected List<Node> generateSuccessors(Node node) {
    454         List<Node> ret = new LinkedList<Node>();
    455         int x = node.x;
    456         int y = node.y;
    457 
    458         // 先遍历X
    459         int tmp = x + xstep;
    460         if (tmp < map.length && tmp >= 0 && !isBarrier(tmp, y))
    461             ret.add(new Node(tmp, y));
    462 
    463         tmp = y + ystep;
    464         if (tmp < map[0].length && tmp >= 0 && !isBarrier(x, tmp))
    465             ret.add(new Node(x, tmp));
    466 
    467         tmp = y - ystep;
    468         if (tmp < map[0].length && tmp >= 0 && !isBarrier(x, tmp))
    469             ret.add(new Node(x, tmp));
    470         
    471         tmp = x - xstep;
    472         if (tmp < map.length && tmp >= 0 && !isBarrier(tmp, y))
    473             ret.add(new Node(tmp, y));
    474 
    475 
    476 
    477         return ret;
    478     }
    479 
    480     public static class Node {
    481         public int x;
    482         public int y;
    483 
    484         public Node(int x, int y) {
    485             this.x = x;
    486             this.y = y;
    487         }
    488 
    489         public String toString() {
    490             return "(" + x + ", " + y + ") ";
    491         }
    492 
    493         public int hashCode() {
    494             // 这里假设x,y都在1000000之内
    495             return x * 1000000 + y;
    496         }
    497 
    498     }
    499 
    500     public class Path implements Comparable {
    501         public Node point;
    502         public int f;
    503         public int g;
    504         public int depth;// 路径的长度
    505         public Path parent;
    506 
    507         /**
    508          * Default c'tor.
    509          */
    510         public Path() {
    511             // parent = null;
    512             // point = null;
    513             // g = f = 0;
    514         }
    515 
    516         /**
    517          * C'tor by copy another object.
    518          * 
    519          * @param p
    520          *            The path object to clone.
    521          */
    522         public Path(Path p) {
    523             // this();
    524             parent = p;
    525             g = p.g;
    526             f = p.f;
    527             depth = p.depth + 1;
    528         }
    529 
    530         /**
    531          * Compare to another object using the total cost f.
    532          * 
    533          * @param o
    534          *            The object to compare to.
    535          * @see Comparable#compareTo()
    536          * @return <code>less than 0</code> This object is smaller than
    537          *         <code>0</code>; <code>0</code> Object are the same.
    538          *         <code>bigger than 0</code> This object is bigger than o.
    539          */
    540         public int compareTo(Object o) {
    541             Path p = (Path) o;
    542             return (f < p.f) ? -1 : (f == p.f ? 1 : 2);
    543             // int i= (int) (f - p.f);
    544             // if (i==0)
    545             // return 1;
    546             // else
    547             // return i;
    548         }
    549 
    550         /**
    551          * Get the last point on the path.
    552          * 
    553          * @return The last point visited by the path.
    554          */
    555         public Node getPoint() {
    556             return point;
    557         }
    558 
    559         public String toString() {
    560             StringBuilder sb = new StringBuilder();
    561             sb.append(point.toString());
    562             Path pa = this.parent;
    563             while (pa != null) {
    564                 sb.append("<-");
    565                 sb.append(pa.point.toString());
    566                 pa = pa.parent;
    567             }
    568             return sb.toString();
    569         }
    570 
    571         /**
    572          * Set the
    573          */
    574         public void setPoint(Node p) {
    575             point = p;
    576         }
    577 
    578         public List<Node> getNodes() {
    579             LinkedList<Node> retPath = new LinkedList<Node>();
    580             // 去头
    581             for (Path i = this; i != null && i.parent != null; i = i.parent) {
    582                 retPath.addFirst(i.getPoint());
    583             }
    584             return retPath;
    585         }
    586         // public int hashCode(){
    587         // return point.x*100+point.y;
    588         // }
    589     }
    590 
    644 
    645     public void clean() {
    646         paths.clear();
    647         mindists.clear();
    648         expandedCounter = 0;
    649     }
    650 
    651 }

    看看main方法,用个例子来看看,看下运行结果

    public static void main(String[] args) {
    
            byte[][] map = new byte[10][3];
    
            System.out
                    .println("Find a path from the top left corner to the right bottom one.");
    
            List<Node> nodes = null;
    
            map[6][1] = 1;
            map[5][1] = 1;
            map[5][2] = 1;
            // map[9][2] = 1;
    
            map[7][1] = 2;
    
            for (int i = 0; i < map.length; i++) {
                for (int j = 0; j < map[0].length; j++)
                    System.out.print(map[i][j] + " ");
                System.out.println();
            }
            long begin = System.currentTimeMillis();
            PathFinder pf = new PathFinder(map);
    //        nodes = pf.getMovePath(new PathFinder.Node(5,1), new PathFinder.Node(7,1),
    //                1, 2, (byte)1);
    
            nodes = pf.getAtkPath(new PathFinder.Node(5,2), new PathFinder.Node(7,0),
                    0, 5, (byte)1);
    
    //        nodes = pf.getMovePath(new PathFinder.Node(2, 0), new PathFinder.Node(
    //                1, 2), 1, 1, (byte) 1);
            // for (int i = 0; i < 5000; i++) {
            // PathFinder pf = new PathFinder(map);
            // nodes = pf.getPaths(new PathFinder.Node(0, 0), new PathFinder.Node(
            // 10, 2), 1, 15, (byte) 1);
            //
            // }
    
            long end = System.currentTimeMillis();
    
            System.out.println("Time = " + (end - begin) + " ms");
            // System.out.println("Expanded = " + pf.getExpandedCounter());
            // System.out.println("Cost = " + pf.getCost());
    
            if (nodes == null)
                System.out.println("No path");
            else {
                System.out.print("Path = ");
                for (Node n : nodes)
                    System.out.print(n);
                System.out.println();
            }
        }

    Find a path from the top left corner to the right bottom one.
    0 0 0
    0 0 0
    0 0 0
    0 0 0
    0 0 0
    0 1 1
    0 1 0
    0 2 0
    0 0 0
    0 0 0
    Time = 0 ms
    Path = (6, 2) (6, 1) (6, 0) (7, 0)

  • 相关阅读:
    java 多线程 Callable -- 分段处理一个大的list 然后再合并结果
    java实现 比较两个文本相似度-- java 中文版 simHash 实现 ,
    spring 多线程 写入数据库 和 写入 xml文件
    爬虫入门 手写一个Java爬虫
    java web 入门级 开发 常用页面调试方法
    Java 递归调用 recursive 给一个参数 返回一大堆
    javaWeb 基础知识
    用 eclipse 创建一个简单的 meaven spring springMvc mybatis 项目
    【题解】【LibreOJ Beta Round #5】游戏 LOJ 531 基环树 博弈论
    【题解】Popping Balls AtCoder Code Festival 2017 qual B E 组合计数
  • 原文地址:https://www.cnblogs.com/dagangzi/p/4738659.html
Copyright © 2011-2022 走看看