使用 javascript 在 n*m 网格中演示 BFS 广度优先搜索算法在带权重图中求最短路径

news2024/11/25 20:23:56

普通的广度优先最短路径搜索算法只能解决无权重的图:

求出的最短路径没问题,但是如果上边的空缺格子的距离为100呢?这种方式搜出的最短路径可能就是错的:

我们可以找一个距离1作为基本距离,然后距离为100的格子在搜索时构筑搜索树结构时可以当成100个虚拟格子来处理,比如该格子本来的code为 xxx,则这100个格子的code分别为code1、code2、code3、...、code99、code100,其中code1~code99的可达格子只能是它的下一个虚拟格子,最尾部的code100的可达格子和正常格子是一样的,不考虑障碍物、超越边界和已经走过的格子的话,分别是其上边、右边、下边、左边的格子( 正常格子或者虚拟格子链表的第一个虚拟格子 ),这样的话这个问题就迎刃而解了,改进后的BFS完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style type="text/css">
        #box1 table{
          border: 0px;
          border-collapse: collapse;
          cursor: pointer;
          background-color: gray;
      }

      #box1 th {
          border: 0px;
          border-collapse: collapse;
          cursor: pointer;
      }

      #box1 td{
          border: 1px solid #000;
          border-collapse: collapse;
          cursor: pointer;
          text-align: center;
          font-size: 4;
      }

      #box1{
          border: 0px;
      }

      .wall{
          background-color: black;
      }

      .startPoint{
          background-color: blue;
      }

      .targetPoint{
          background-color: blue;
      }

      .line{
          background-color: yellow;
      }
    </style>
</head>
<body>
    单击灰色格子将其设置为障碍( 黑色 ),单击障碍清空障碍,双击格子修改格子的距离
    <div id="box1"></div>
    <button onclick="startSearch()">开始寻路</button>
    <button onclick="initRingWalls()">生成环状障碍</button>
    <button onclick="initPointWalls()">生成点状障碍</button>
    <button onclick="clearLine()">清空路线</button>
</body>
<script type="text/javascript">

    var map_code_td = new Map();
    var row_count = 20;
    var col_count = 20;
    var rowNum_start = 1;
    var colNum_start = 1;
    var rowNum_target = 10;
    var colNum_target = 10;
    var map_code_wallYn = new Map();
    var map_code_distance = new Map();
    var code_start = rowNum_start + "_" + colNum_start;
    var code_target = rowNum_target + "_" + colNum_target;

    // 辅助帮助显示横竖纹的底色棋盘,没有任何事件和业务逻辑
    function initBox1( ) {
        var table = document.createElement("table");
        table.rules  = 'all' ;
        for (var rowNum = 1; rowNum <= row_count; rowNum++) {
            var tr = document.createElement("tr");
            for (var colNum = 1; colNum <= col_count; colNum++) {
                var td = document.createElement("td");
                td.width = 20;
                td.height = 20;
                td.className ="road";
                tr.appendChild(td);
                var code = rowNum + "_" + colNum;
                map_code_td.set(code, td);

                if( code != code_start && code != code_target ){
                    map_code_distance.set(code, 1);
                    td.innerText = "1";
                }
            }
            table.appendChild(tr);
        }
        document.getElementById( "box1" ).appendChild(table);
        table.addEventListener( "click", setOrClearWall );
        table.addEventListener( "dblclick", setDistance );

        var code_start = rowNum_start + "_" + colNum_start;
        var code_target = rowNum_target + "_" + colNum_target;

        map_code_td.get( code_start ).className = "startPoint";
        map_code_td.get( code_target ).className = "targetPoint";
    }

    function setDistance(){
        var td = event.srcElement;
        var rowNum = td.parentElement.rowIndex + 1;
        var colNum = td.cellIndex + 1;
        var code = rowNum + "_" + colNum;

        var distance =  prompt("请为格子 " + code + " 设置距离", "1");
        if (distance != null && distance != "") {
            distance = parseInt(distance);
            if (distance > 0) {
                map_code_distance.set(code, distance);
                td.innerText = distance;
            }
        } else {
            alert("您没有输入距离!");
        }
    }

    function initPointWalls(){
         // 清空障碍
        clearAllWalls();

        for (var rowNum = 1; rowNum <= row_count; rowNum++) {
            for (var colNum = 1; colNum <= col_count; colNum++) {
                var code = rowNum + "_" + colNum;
                if( code == code_start || code == code_target ){
                    continue;
                }
                // 右 1/3 的几率成为障碍
                var randomNum = randomBetween(1,10000);
                if( randomNum < 3333  ){
                    var td = map_code_td.get( code );
                    td.className = "wall";
                    map_code_wallYn.set( code, 'y' );
                }
            }
        }
    }

    // 鼠标的点击事件监听方法,用来将点击的格子设置为墙
    function setOrClearWall(){
        var td = event.srcElement;
        var rowNum = td.parentElement.rowIndex + 1;
        var colNum = td.cellIndex + 1;
        var code = rowNum + "_" + colNum;
        if( td.className == "wall" ){
            td.className = "road";
            map_code_wallYn.set( code, "n" );
        }else if( td.className =='road' ){
            td.className = "wall";
            map_code_wallYn.set( code, "y" );
        }
    }

    function randomBetween(minNum,maxNum){
        return Math.floor(Math.random() * (maxNum - minNum + 1)) + minNum;
    }

    function clearAllWalls(){
        for (var rowNum = 1; rowNum <= row_count; rowNum++) {
            for (var colNum = 1; colNum <= col_count; colNum++) {
                var code = rowNum + "_" + colNum;
                if( code == code_start  || code == code_target){
                    continue;
                }
                var wallYn = map_code_wallYn.get( code );
                if( wallYn == 'y' ){
                    map_code_td.get( code ).className = "";
                    map_code_wallYn.set( code, "n" );
                }else{
                    var td = map_code_td.get( code );
                    if( td.className == 'passed' ){
                        td.className = "";
                    }
                }
            }
        }
    }

    function initRingWalls(){
        // 清空障碍
        clearAllWalls();

        // 将第三行设置为障碍
        var circles = [2,4,6,8,10,12,14,16,18];
        for( var i in circles ){
            var circle = circles[i];
            var randomNum = randomBetween(1,4);
            var randomNum_1 = randomBetween(circle,col_count - circle);
            var randomNum_2 = randomBetween(circle,col_count - circle);
            var randomNum_3 = randomBetween(circle,row_count - circle);
            var randomNum_4 = randomBetween(circle,row_count - circle);
            // 顶行
            for (var colNum = circle; colNum <= ( col_count - circle );colNum++){
                if( randomNum == 1 ){
                    if( colNum == randomNum_1 ){
                        continue;
                    }
                }
                var code = circle + "_" + colNum;
                map_code_td.get( code ).className = "wall";
                map_code_wallYn.set( code, "y" );
            }

            // 底行
            for (var colNum = circle; colNum <= ( col_count - circle );colNum++){
                if( randomNum == 2 ){
                    if( colNum == randomNum_2 ){
                        continue;
                    }
                }
                var code = ( row_count - circle ) + "_" +  colNum;
                map_code_td.get( code ).className = "wall";
                map_code_wallYn.set( code, "y" );
            }

            // 左列
            for (var rowNum = circle; rowNum <= ( row_count - circle );rowNum++){
                if( randomNum == 3 ){
                    if( rowNum == randomNum_3 ){
                        continue;
                    }
                }
                var code = rowNum + "_" + circle;
                map_code_td.get( code ).className = "wall";
                map_code_wallYn.set( code, "y" );
            }

            // 右列
            for (var rowNum = circle; rowNum <= ( row_count - circle );rowNum++){
                if( randomNum == 4 ){
                    if( rowNum == randomNum_4 ){
                        continue;
                    }
                }
                var code = rowNum + "_" + ( col_count - circle );
                map_code_td.get( code ).className = "wall";
                map_code_wallYn.set( code, "y" );
            }
        }
    }

    // 检测 code 表示的格子是否可以通行
    function canPass( code ){
        var arr = code.split("_");
        var rowNum = parseInt( arr[0])
        var colNum = parseInt( arr[1] );
        if( rowNum > row_count || rowNum < 1 || colNum > col_count || colNum < 1 ){
            // 该位置已经超过边界了,不能走
            return false;
        }
        var wallYn = map_code_wallYn.get( code );
        if( wallYn == 'y' ){
            // 该位置是墙,不能走
            return false;
        }
        return true;
    }

    class Node {
        constructor(code,virtualCode,isVirtualCode,isVirtualHead,isVirtualTail,distance,orderNum,parent) {
            // 形如:'1_1'
            this.code = code;

            // 形如: '1_1_1'
            this.virtualCode = virtualCode;

            // 是否是虚拟节点
            this.isVirtualCode = isVirtualCode;

            // 是否是该虚拟链上的头节点
            this.isVirtualHead = isVirtualHead;

            // 是否是该虚拟链上的尾结点
            this.isVirtualTail = isVirtualTail;

            // 该训链的距离
            this.distance = distance;

            // 该虚拟节点是该虚拟链中的第几个虚拟节点,1~distance
            this.orderNum = orderNum;

            this.children = [];
            this.parent = parent;
        }

        addChild(node) {
            this.children.push(node);
        }
    }

    initBox1(  );


    function isNormalCode( code ){
        if( code.split.length > 2 ){
            // 是虚拟格子,形如:"1_1_2"
            return false;
        }
        // 形如:"1_1"
        return true;
    }

    function startSearch(  ){
        var node_start = new Node( code_start,null,false,false,false,1,null,null )
        var nodes_currLevel = [node_start];
        var nodes_nextLevel = [];
        var codes_passed = [];
        var node_target = null;
        while( true ){
            // 如果找到了 目标节点或者 nodes_currLevel 为空了,需要退出循环
            // 遍历 nodes_currLevel 中的 每个 node,检测其是否是目标节点,如果是目标节点,则找到了最短路径,
            if( nodes_currLevel.length == 0 ){
                break;
            }
            for( var i in nodes_currLevel ){
                var node = nodes_currLevel[i];
                var code = node.code;
                var isVirtualCode = node.isVirtualCode;
                if( isVirtualCode ){
                    // 是虚拟节点
                    handleVirtualNode( node,nodes_nextLevel,codes_passed );
                }else{
                    // 是正常节点
                    node_target = handleNormalNode( node,nodes_nextLevel,codes_passed );
                }
                if( node_target != null ){
                    break;
                }
            }
            if( node_target != null ){
                break;
            }
            // 该层遍历完了,更新 nodes_currLevel 和重置 nodes_nextLevel
            nodes_currLevel = nodes_nextLevel;
            nodes_nextLevel = [];
        }
        if( node_target != null ){
            // console.log("找到最短路径了,node_target = ",node_target);
            // 画出最短路径
            printPath( node_target );
        }else{
            alert( "未找到最短路径" );
        }
    }

    // node 是虚拟节点
    function handleVirtualNode( node,nodes_nextLevel,codes_passed ){
        var virtualCode = node.virtualCode;
        var code = node.code;
        var distance = node.distance;
        var isVirtualTail = node.isVirtualTail;
        var orderNum = node.orderNum;

        if( codes_passed.indexOf( virtualCode ) > -1 ){
            // 该节点已经遍历过了,不需要重复处理
            return;
        }

        // 找到该节点的所有可达子节点,保存到下一级的集合中,并将当前节点置为已遍历状态
        if( isVirtualTail ){
            // todo 该虚拟节点是所处虚拟链的尾巴
            // 找到该虚拟节点所在的正常节点的上、右、下、左节点( 需要排除超过边界的和是障碍物的,可能下个节点也是距离大于1 的,这样的话,需要让其的虚拟头部加入nodes_nextLevel中 )
            var distance = map_code_distance.get( code );
            var arr = code.split("_");
            var rowNum = parseInt( arr[0] );
            var colNum = parseInt( arr[1] );

            var code_up = ( rowNum - 1 ) + "_" + colNum;
            var code_right = rowNum + "_" + ( colNum + 1 );
            var code_down = ( rowNum + 1 ) + "_" + colNum;
            var code_left = rowNum + "_" + ( colNum - 1 );

            var node_up = buildNodeOrVirtualNodeByCode( code_up );
            var node_right = buildNodeOrVirtualNodeByCode( code_right );
            var node_down = buildNodeOrVirtualNodeByCode( code_down );
            var node_left = buildNodeOrVirtualNodeByCode( code_left );

            // 更新搜索树结构
            flushNodeTree( node,node_up,nodes_nextLevel );
            flushNodeTree( node,node_right,nodes_nextLevel );
            flushNodeTree( node,node_down,nodes_nextLevel );
            flushNodeTree( node,node_left,nodes_nextLevel );
        }else{
            // todo 该虚拟节点是所处虚拟链的头部或身体,下一步的可达节点为该虚拟链的下一个虚拟节点,
            var arr = code.split("_");
            var rowNum = parseInt( arr[0] );
            var colNum = parseInt( arr[1] );

            var code_up = ( rowNum - 1 ) + "_" + colNum;
            var code_right = rowNum + "_" + ( colNum + 1 );
            var code_down = ( rowNum + 1 ) + "_" + colNum;
            var code_left = rowNum + "_" + ( colNum - 1 );

            var orderNum_next = orderNum + 1;
            var virtualCode_next = code + "_" + orderNum_next;

            var isVirtualTail_next = false;
            if( orderNum_next == distance ){
                isVirtualTail_next = true;
            }
            var node_next = new Node( code,virtualCode_next,true,false,isVirtualTail,distance,orderNum_next,node );
            // 更新搜索树结构
            flushNodeTree( node,node_next,nodes_nextLevel );
        }
        codes_passed.push( virtualCode );
    }

    function  flushNodeTree( node,node_next,nodes_nextLevel ){
        if( node_next == null ){
            return;
        }
        // 更新树结构
        node_next.parent = node;
        node.addChild( node_next );
        nodes_nextLevel.push( node_next );
    }

    // code 的格式为:'${rowNum}_${colNum}'
    function buildNodeOrVirtualNodeByCode( code ){
        if( !canPass( code ) ){
            return null;
        }
        var distance = map_code_distance.get( code );
        if( distance == 1 ){
            // 返回正常节点
            return new Node( code,null,false,false,false,1,null,null );
        }else{
            // 返回虚拟节点( 所处虚拟链的头部 )
            return new Node( code,code + "_1",true,true,false,distance,1,null );
        }
    }

    // node 是正常节点
    function handleNormalNode( node,nodes_nextLevel,codes_passed ){
        var code = node.code;
        if( codes_passed.indexOf( code ) > -1 ){
            // 该节点已经遍历过了,不需要重复处理
            return;
        }
        if( code == code_target ){
            // 找到目标节点了
            return node;
        }
        // 找到该节点的所有可达子节点,保存到下一级的集合中,并将当前节点置为已遍历状态
        // 找到该节点的上、右、下、左节点( 需要排除超过边界的和是障碍物的,可能这些几点是距离大于1的,需要用其对应的虚拟链的头部替代 )
        var arr = code.split("_");
        var rowNum = parseInt( arr[0] );
        var colNum = parseInt( arr[1] );

        var code_up = ( rowNum - 1 ) + "_" + colNum;
        var code_right = rowNum + "_" + ( colNum + 1 );
        var code_down = ( rowNum + 1 ) + "_" + colNum;
        var code_left = rowNum + "_" + ( colNum - 1 );

        var node_up = buildNodeOrVirtualNodeByCode( code_up );
        var node_right = buildNodeOrVirtualNodeByCode( code_right );
        var node_down = buildNodeOrVirtualNodeByCode( code_down );
        var node_left = buildNodeOrVirtualNodeByCode( code_left );

        // 更新搜索树结构
        flushNodeTree( node, node_up, nodes_nextLevel );
        flushNodeTree( node, node_right, nodes_nextLevel );
        flushNodeTree( node, node_down, nodes_nextLevel );
        flushNodeTree( node, node_left, nodes_nextLevel );

        codes_passed.push( code );
    }

    function printPath( node_target ){
        if( node_target == null ){
            return;
        }
        var node_prev = node_target.parent;
        while( true ){
            if( node_prev == null ){
                break;
            }
            var code = node_prev.code;
            if( code == code_start ){
                break
            }
            var td = map_code_td.get( code );
            td.className = "line";
            node_prev = node_prev.parent;
        }
    }

    function clearLine(){
        for( var rowNum=1;rowNum<=row_count;rowNum++ ){
            for( var colNum=1;colNum<=col_count;colNum++ ){
                var code = rowNum + "_" + colNum;
                var td = map_code_td.get( code );
                if( td.className == "line" ){
                    td.className = "";
                }
            }
        }
    }
</script>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1162680.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[免费] 适用于 Windows的10 的十大数据恢复软件

Windows 10是微软开发的跨平台和设备应用程序的操作系统。它启动速度更快&#xff0c;具有熟悉且扩展的“开始”菜单&#xff0c;甚至可以在多种设备上以新的方式工作。所以&#xff0c;Windows 10非常流行&#xff0c;我们用它来保存我们的照片、音乐、文档和更多文件。但有时…

高效剪辑:视频剪辑新手如何批量分割视频

随着社交媒体和自媒体的兴起&#xff0c;视频剪辑已经成为一项必备的技能。无论是制作自己的电影、视频博客&#xff0c;还是为广告宣传提供支持&#xff0c;高效地剪辑视频都是非常重要的。对于视频剪辑新手来说&#xff0c;了解如何批量分割视频是提高剪辑效率的关键。本文讲…

左右联动 provide、inject+props、emit

实现效果 成都大屏 —— 视频管理 左右两个都是组件&#xff0c;所以涉及到父组件向多个子组建传递数据 方法1 provide、inject 目录结构 父组件 index.vue 用provide传递数据和修改数据的方法 import { provide} from "vue"; provide("Provide_Selected…

C语言队列实现

1.知识百科 队列是一种特殊的线性表&#xff0c;特殊之处在于它只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08;rear&#xff09;进行插入操作&#xff0c;和栈一样&#xff0c;队列是一种操作受限制的线性表。进行插入操作的…

月入8K, 计算机专业应届女孩转行5G网络优化工程师,她说:这行请慎入

小C说&#xff0c;如果要用两个字描述23年计算机专业的就业心情&#xff0c;那就是“焦虑”&#xff1b;用三个字描述23年计算机专业的就业环境&#xff0c;那就是“卷麻了”。 得益于张雪峰老师的就业推荐计算机专业需求的日益减少&#xff0c;2023年&#xff0c;计算机专业成…

【漏洞复现】74cms任意文件读取

漏洞描述 74CMS 是一款国内用的比较多招聘网站管理系统&#xff08;Job Board CMS&#xff09;&#xff0c;专注于招聘和人力资源领域的网站建设&#xff0c;存在任意文件读取漏洞 免责声明 技术文章仅供参考&#xff0c;任何个人和组织使用网络应当遵守宪法法律&#xff0c…

vue3中使用deck.gl

deck,gl网址&#xff1a;Home | deck.gl 因为deck.gl是国外的技术&#xff0c;国外最流行的框架是react&#xff0c;所以deck.gl有为react提供的地图组件&#xff0c;没有为vue提供&#xff0c;并且还需要翻墙。所以想用vue使用这个还是有一定难度的。 除了用到deck.gl之外还…

使用Serv-U FTP服务器共享文件,实现无公网IP的外网访问

文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 科技日益发展的今天&#xff0c;移动电子设备似乎成了我们生活的主角&#xff0c;智能…

vue项目打包时按一定的名称规范生成对应的压缩包

在项目部署中经常需要将打包的dist按一定的名称压缩成压缩包&#xff0c;今天记录一下打包时生成压缩包的过程。其中有用到的npm包需要自己安装一下。 js文件放置的目录如下 compress.js内容如下&#xff1a; // compress.jsimport fs from "fs"; import shell fro…

电商价格数据监测接口|淘宝商品价格接口|天猫商品价格接口|京东商品价格接口|拼多多商品价格接口|API接口申请指南

电商价格数据监测接口是一种可以实时监测电商平台上商品价格的接口工具。通过这个接口&#xff0c;可以获取到各个电商平台的商品价格信息&#xff0c;并且可以设置价格监控频率、智能数据绑定、破价提醒机制等功能。 以下是电商价格数据监测接口的一些特点&#xff1a; 商城…

“恒山光量子”首秀!玻色量子联合移动云发表物理1区Top期刊SCPMA论文

2023年5月&#xff0c;北京玻色量子科技有限公司&#xff08;以下简称“玻色量子”&#xff09;联合移动云在我国知名科技期刊平台《中国科学&#xff1a;物理学 力学 天文学》英文版上发表了以“Optical experimental solution for the multiway number partitioning problem …

网络工程师进阶课:华为HCIP认证课程介绍

微思网络HCIP VIP试听课程&#xff1a;DHCP协议原理与配置https://www.bilibili.com/video/BV1cy4y1J7yg/?spm_id_from333.999.0.0 【微|信|公|众|号&#xff1a;厦门微思网络】 【微思网络http://www.xmws.cn&#xff0c;成立于2002年&#xff0c;专业培训21年&#xff0c;思…

获评AI基础软件「领导者」,九章云极DataCanvas公司技术创新能力最强!

近日&#xff0c;弗若斯特沙利文&#xff08;Frost & Sullivan&#xff0c;简称“沙利文”&#xff09;正式发布《中国AI基础软件市场研究报告&#xff08;2023&#xff09;》&#xff0c;公布当下对中国AI基础软件发展创新的最新洞察。九章云极DataCanvas公司获评AI基础软…

@所有人,城市燃气信息化与信息安全建设方法

关键词&#xff1a;城市燃气信息化、智慧燃气建设、城市燃气安全、智慧燃气、智慧燃气平台 近几年&#xff0c;燃气作为一种新兴的燃料迅速普及开来&#xff0c;和燃气有关的企业之间的竞争也不可避免。身处在互联网的时代&#xff0c;企业只有顺应时代的潮流&#xff0c;将城…

zookeeper集群选举机制

Zookeeper选举机制——第一次启动 zookeeper集群三个重要的参数&#xff08;决定选举结果&#xff09; SID &#xff1a; 服务器 ID 。 用来唯一标识一台 ZooKeeper集群中的机器&#xff0c;每台机器不能重 &#xff0c; 和 myid 一致 。 ZXID &#xff1a;事务 ID 。 ZXID 是…

室内外导航一体化技术原理与成品展示

随着人们对出行需求的不断提升&#xff0c;室内外导航一体化技术变得越来越重要。该技术可以帮助用户在建筑物内、外部空间以及城市范围内进行精确导航&#xff0c;提升出行体验。 室内外一体化导航实现技术原理 室内外导航技术主要依赖于定位技术、地图数据以及空间建模等手段…

YOLOV8 NANO手势识别

采用YOLOV8 NANO训练&#xff0c;得到pt模型&#xff0c;然后转换成ONNX模型&#xff0c;OPENCV DNN调用&#xff0c;支持C,PYTHON,ANDROID开发。CPU每帧20MS左右&#xff0c;可以达到实时性 YOLOV8NANO手势识别

保护隐私,打造独特的个人图床——cpolar+Qchan轻量级搭建——“cpolar内网穿透”

文章目录 前言1. Qchan网站搭建1.1 Qchan下载和安装1.2 Qchan网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar云端设置2.2 Cpolar本地设置 3. 公网访问测试总结 前言 图床作为云存储的一项重要应用场景&#xff0c;在大量开发人员的努力下&#xff0c;已经开发出大…

文言一心中将C语言归类为低级语言,这对么?

文言一心中将C语言归类为低级语言&#xff0c;这对么? 以下是文言一心中的回答&#xff1a;C语言属于低级语言。低级语言通常指的是接近于机器语言的编程语言&#xff0c;它们与计算机硬件的交互更加直接&#xff0c;能够更高效地利用计算机资源。最近很多小伙伴找我&#xff…

SSM使用OpenOffice+Adobe acrobat实现Office文件的在线预览

文章迁移自语雀。 也许Java天生不适合处理Office文件吧&#xff0c;POI的使用一堆问题&#xff0c;现在SpringMVCSpringMybatis的web项目想实现在线预览也是问题一大堆。马的,开始时打算使用OpenOfficeSWFToolsFlexPaper的&#xff0c;但是该方案是使用flash的&#xff0c;众所…