算法8.从暴力递归到动态规划1
算法|8.从暴力递归到动态规划1
目前感觉,背包问题和货币数组问题本质相同,货币的与dp相关的三种代码写完了,快复习不完了,背包暂时先不写了,回头再写,补充,再总结,结合那个C++大神的文章再弄。
背包类问题
目前来讲,我接触到的背包问题有四种分别是01背包、完全背包、多重背包、以及部分背包。背包问题都属于NP问题(非直接求解问题),前三种一般使用动态规划进行求解,后一种一般使用贪心求解。
01背包
题意:给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,装的物品不能超过这个重量。返回能装下的最大价值
解题思路:
- 先写出递归版本,然后对照着改dp。
- 要与不要。结果越大越好,是正向决策,同时使用的累加。
- 参数设置:重量数组w、价值数组v、当前决策到的坐标index、背包剩余的空间rest
- 可变参数为index和rest,所以dp表是一张二维表。
核心代码:
递归代码:
public static int maxValue(int[] w, int[] v, int bag) {if(w==null||v==null||w.length!=v.length||w.length==0){return 0;}return process(w,v,0,bag);
}public static int process(int[] w, int[] v, int index, int rest) {if(rest<0){return -1;}if(index==w.length){return 0;}int p1=process(w,v,index+1,rest);int p2=0;int next=process(w,v,index+1,rest-w[index]);if(next!=-1){p2=v[index]+next;}return Math.max(p1,p2);
}
dp代码:
public static int dp(int[] w, int[] v, int bag) {if (w == null || v == null || w.length != v.length || w.length == 0) {return 0;}int N=w.length;int[][] dp=new int[N+1][bag+1];for (int index = N-1; index >=0 ; index--) {for (int rest = 0; rest <=bag ; rest++) {int p1=dp[index+1][rest];int p2=0;int next=rest-w[index]<0?-1:dp[index+1][rest-w[index]];if(next!=-1){p2=v[index]+next;}dp[index][rest]=Math.max(p1,p2);}}return dp[0][bag];
}
测试代码:
public static void main(String[] args) {int[] weights = { 3, 2, 4, 7, 3, 1, 7 };int[] values = { 5, 6, 3, 19, 12, 4, 2 };int bag = 15;System.out.println(maxValue(weights, values, bag));System.out.println(dp(weights, values, bag));
}
测试结果:
完全背包
题意:有 N种物品和一个容量为C的背包,每种物品都有无限件。第 i件物品的体积是 v[i],价值是w[i] .求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
解题思路:
核心代码:
测试代码:
测试结果:
多重背包
题意:有 N种物品和一个容量为C的背包,数量为s[i]。第 i件物品的体积是 v[i],价值是w[i] .求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
解题思路:
核心代码:
测试代码:
测试结果:
货币类问题
组成aim的方法数(每张认为是一种)
题意:arr是货币数组,其中的值都是正数。再给定一个正数aim。
每个值都认为是一张货币,即便是值相同的货币也认为每一张都是不同的,
返回组成aim的方法数。例如:arr = {1,1,1},aim = 2。
第0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2
一共就3种方法,所以返回3
解题思路:
- 这一题其实和01背包问题很像,只是这个是要求正好组成aim,01背包则是不超过的方法数
- 所以这里我们只需要在aim=0时返回1,总金额超过了|根本就组不成(钱不够)就返回0
- 注意:改写过程中三目操作符建议加上括号(血淋淋的教训…)//
dp[index][rest] = dp[index + 1][rest] + (rest - arr[index]) < 0 ? 0 : dp[index + 1][rest - arr[index]];
核心代码:
暴力递归:
public static int coinWays(int[] arr, int aim) {return process(arr, 0, aim);
}public static int process(int[] arr, int index, int rest) {if (rest < 0) {return 0;}if (index == arr.length) {return rest == 0 ? 1 : 0;}return process(arr, index + 1, rest - arr[index])+ process(arr, index + 1, rest);
}
dp:
public static int dp(int[] arr, int aim) {if (aim == 0) {return 1;}int N = arr.length;int[][] dp = new int[N + 1][aim + 1];dp[N][0] = 1;for (int index = N - 1; index >= 0; index--) {for (int rest = 0; rest <= aim; rest++) {
// dp[index][rest] = dp[index + 1][rest] + (rest - arr[index]) < 0 ? 0 : dp[index + 1][rest - arr[index]];dp[index][rest] = dp[index + 1][rest] + (rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : 0);}}return dp[0][aim];}
测试代码:
略
测试结果:
组成aim的方法数(每种张数无限)
题意:arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。返回组成aim的方法数
例如:arr = {1,2},aim = 4 方法如下:1+1+1+1、1+1+2、2+2
一共就3种方法,所以返回3。
解题思路:
- 大体思路和上边相同,只不过子过程需要对要用多少张数进行遍历
- 张数遍历时循环条件为zhang * arr[index] <= rest,对应的dp改写中也需要遍历(如果不优化的,优化之后再说,这里应该是可以进行斜率优化)
核心代码:
递归:
public static int coinsWay(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0;}return process(arr, 0, aim);
}public static int process(int[] arr, int index, int rest) {if(rest<0){return 0;}if(index==arr.length){return rest==0?1:0;}int ways=0;for (int zhang = 0; zhang*arr[index] < rest ; zhang++) {ways+=process(arr,index+1,rest-zhang*arr[index]);}return ways;
}
dp:
public static int dp(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0;}int N = arr.length;int[][] dp = new int[N + 1][aim + 1];dp[N][0] = 1;for (int index = N - 1; index >= 0; index--) {for (int rest = 0; rest <= aim; rest++) {int ways=0;for (int zhang = 0; zhang*arr[index] < rest ; zhang++) {ways+=(rest-zhang*arr[index]<0)? 0: dp[index+1][rest-zhang*arr[index]];}dp[index][rest]=ways;}}return dp[0][aim];
}
测试代码:
略
测试结果:
组成aim的方法数(每种张数有限)
题意:arr是货币数组,其中的值都是正数。再给定一个正数aim。每个值都认为是一张货币,认为值相同的货币没有任何不同,返回组成aim的方法数
例如:arr = {1,2,1,1,2,1,2},aim = 4方法:1+1+1+1、1+1+2、2+2
一共就3种方法,所以返回3
解题思路:
- 本题思路与上题类似,只是张数变成有限的了,对应的遍历张数的条件多了一个
- 另外,本题不是给两个数组一个张数组和值数组,所以我们还需要对数据进行预处理,封装,并进行数据统计,并提供对应方法让外部调用。
- 封装构造方法初始化大小确定(我们给的);getInfo是我们进行的词频统计,根据arr,涉及到containKey,put等方法
核心代码:
递归代码:
public static class Info {private int[] coins;private int[] zhangs;public Info(int[] coins, int[] zhangs) {this.coins = coins;this.zhangs = zhangs;}
}public static Info getInfo(int[] arr) {HashMap<Integer, Integer> map = new HashMap<>();for (int num : arr) {if (map.containsKey(num)) {map.put(num, map.get(num) + 1);} else {map.put(num, 1);}}int N = map.size();int[] coins = new int[N];int[] zhangs = new int[N];int index = 0;for (Map.Entry<Integer, Integer> entry : map.entrySet()) {coins[index] = entry.getKey();zhangs[index] = entry.getValue();}Info info = new Info(coins, zhangs);return info;
}public static int coinsWay(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0;}Info info = getInfo(arr);return process(info.coins, info.zhangs, 0, aim);
}public static int process(int[] coins, int[] zhangs, int index, int rest) {if (rest < 0) {return 0;}if (index == coins.length) {return rest == 0 ? 1 : 0;}int ways = 0;for (int zhang = 0; zhang < zhangs.length && (zhang * coins[index] < rest); zhang++) {ways += process(coins, zhangs, index + 1, rest - zhang * coins[index]);}return ways;
}
dp代码:
public static int dp(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return 0;}Info info = getInfo(arr);int[] coins = info.coins;int[] zhangs = info.zhangs;int N = coins.length;int[][] dp = new int[N + 1][aim + 1];dp[N][0] = 1;for (int index = N - 1; index >= 0; index--) {for (int rest = 0; rest <= aim; rest++) {int ways = 0;for (int zhang = 0; zhang < zhangs.length && (zhang * coins[index] < rest); zhang++) {ways += (rest - zhang * coins[index] < 0) ? 0 : dp[index + 1][rest - zhang * coins[index]];}dp[index][rest] = ways;}}return dp[0][aim];
}
测试代码:略
测试结果:
组成aim的最小货币数(每张张数无限)
题意:arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。返回组成aim的最少货币数。
解题思路:
- 还是需要对张数进行遍历,只不过只有一个条件
- 接受结果值设为整数最大值,最终结果返回较小值
- 另外另外,边界条件不满足条件的值需要修改成最大值,不难咱们得犯大难了,遭老罪了!要不然你计算的时候把没有组成aim的,但是张数更少的算里边了,肯定错啊;rest==0时,不需要货币,即使满足也不需要了,所以记得改成0
补充:
- 这里其实可以对数组按照货币值进行预处理/排序(使用迭代给sort传比较器//优先级队列)
- 面值数组,不需要预处理,只需要用于降序排列的比较器
核心代码:
递归代码:
public static int minCoins(int[] arr, int aim) {if(aim==0){return 0;}if (arr == null || arr.length == 0 || aim < 0) {return Integer.MAX_VALUE;}return process(arr, 0, aim);
}public static int process(int[] arr, int index, int rest) {if (rest < 0) {return Integer.MAX_VALUE;}if (index == arr.length) {return rest == 0 ? 0 : Integer.MAX_VALUE;}int ans = Integer.MAX_VALUE;for (int zhang = 0; zhang * arr[index] < rest; zhang++) {int next = process(arr, index + 1, rest - zhang * arr[index]);if (next != Integer.MAX_VALUE && next > 0) {//要不然最大值加最大值可能滚成负数ans = Math.min(next, ans);}}return ans;
}
dp代码:
public static int dp(int[] arr, int aim) {if (aim == 0) {return 0;}int N = arr.length;int[][] dp = new int[N + 1][aim + 1];dp[N][0] = 0;for (int j = 1; j <= aim; j++) {dp[N][j] = Integer.MAX_VALUE;}for (int index = N - 1; index >= 0; index--) {for (int rest = 0; rest <= aim; rest++) {int ans = Integer.MAX_VALUE;for (int zhang = 0; zhang * arr[index] < rest; zhang++) {int next = (rest - zhang * arr[index] < 0) ? Integer.MAX_VALUE : dp[index + 1][rest - zhang * arr[index]];if (next != Integer.MAX_VALUE && next > 0) {//要不然最大值加最大值可能滚成负数ans = Math.min(next, ans);}}dp[index][rest]=ans;}}return dp[0][aim];
}
测试代码:略
测试结果:
从左到右尝试模型总结1
改写规则:
- 确定可变参数个数——dp是几维表
- 确定可变参数的变化范围——是0N还是0N-1
- 预处理(边界条件)
- 确定普遍位置怎么确定
- 边界判断——使用三目时一定要注意加括号😥
- 四个角中的哪个是最终结果
例题总结:
- 01背包——边界判断:超重是-1,没装够/刚好是0;要和不要的两种情况pk,要较小值
- 完全背包
- 多重背包
- 组成aim的方法数(每张认为是一种)——边界判断:超支/不够都是0,aim=0时index<=arr.length都算是1;两种情况不需要pk,直接相加返回
- 组成aim的方法数(每种张数无限)——边界判断:超支/不够都是0,aim=0时index<=arr.length都算是1;不是两种情况了,对有效的张数(一个条件)进行遍历,总方法相加
- 组成aim的方法数(每种张数有限)——边界判断:超支/不够都是0,aim=0时index<=arr.length都算是1;不是两种情况了,对有效的张数(两个条件)进行遍历,总方法相加
- 组成aim的最小货币数(每张张数无限)——边界条件判断:初值都为最大值,除了aim=0时,递归那aim=0一定在非法条件的前边,next值有效的判断;两种情况pk,要最小的
三种dp解法背包问题区别:
略
前三种dp解法货币数组区别:
注:这里返回的都是方法数,肯定是越多越好,不涉及边界值返回的系列问题。
- 货币数组类型决定了是否需要张数遍历(面值 不用)
- 张数有限无限决定了张数遍历的条件是1个还是两个
- 一般都是index倒序,rest正序
注:区分面值数组、货币数组
前者是天然去重,后者可能存在相同的,看题目设定决定是否需要进行预处理
另本类型开头的那种,其实也算是面值数组了。
两种情况了,对有效的张数(一个条件)进行遍历,总方法相加
- 组成aim的方法数(每种张数有限)——边界判断:超支/不够都是0,aim=0时index<=arr.length都算是1;不是两种情况了,对有效的张数(两个条件)进行遍历,总方法相加
- 组成aim的最小货币数(每张张数无限)——边界条件判断:初值都为最大值,除了aim=0时,递归那aim=0一定在非法条件的前边,next值有效的判断;两种情况pk,要最小的
三种dp解法背包问题区别:
略
前三种dp解法货币数组区别:
注:这里返回的都是方法数,肯定是越多越好,不涉及边界值返回的系列问题。
- 货币数组类型决定了是否需要张数遍历(面值 不用)
- 张数有限无限决定了张数遍历的条件是1个还是两个
- 一般都是index倒序,rest正序
注:区分面值数组、货币数组
前者是天然去重,后者可能存在相同的,看题目设定决定是否需要进行预处理
另本类型开头的那种,其实也算是面值数组了。
相关文章:

算法8.从暴力递归到动态规划1
算法|8.从暴力递归到动态规划1 目前感觉,背包问题和货币数组问题本质相同,货币的与dp相关的三种代码写完了,快复习不完了,背包暂时先不写了,回头再写,补充,再总结,结合那个C大神的文…...

8-JDBC 编程
目录 1.数据库编程的必备条件 PS:程序是怎么操作数据库的? 2.什么是JDBC? 2.1.JDBC定义 2.2.JDBC工作原理 3.JDBC使用 3.1.创建项目并添加MySQL驱动包 3.2.使用代码操作数据库 3.2.1.获得数据源 3.2.2.获得连接 3.2.3.获得执行器 …...

零基础如何学习 Web 安全?
Web安全不仅是互联网的核心,而且还是云计算和移动互联网的最佳载体。对于信息安全从业者而言,Web安全是一个非常重要的研究课题之一。 Web应用是指采用B/S架构、通过HTTP/HTTPS协议提供服务的统称。随着互联网的广泛使用,社交网络、聊天工具…...

【简单实用框架】【AddressablesMgr】【可移植】
☀️博客主页:CSDN博客主页💨本文由 萌萌的小木屋 原创,首发于 CSDN💢🔥学习专栏推荐:面试汇总❗️游戏框架专栏推荐:游戏实用框架专栏⛅️点赞 👍 收藏 ⭐留言 📝&#…...
android 12.0Launcher3禁止拖拽app图标到第一屏
1.概述 在12.0进行定制化开发Launcher3中,会对Launcher3 做些要求,比如现在的需求就是Launcher3第一屏的图标固定,不让其他屏的图标拖动到 第一屏所以说这个需求和 禁止拖拽图标到Hotseat类似,也是从WorkSpace.java里面寻找解决方案 2.Launcher3禁止拖拽app图标到第一屏相…...
SkyLine简介
简介 SkyLine产品系列(TerraExplorer 、TerraGate、TerraBuilder)是一套优秀的三维数字地球平台软件。凭借其国际领先的三维数字化显示技术,它可以利用海量的遥感航测影像数据、数字高程数据以及其他二三维数据搭建出一个对真实世界进行模拟…...

算法基础学习笔记——④前缀和\差分\双指针\位运算
✨博主:命运之光 ✨专栏:算法基础学习 目录 ✨前缀和 ✨一维前缀和 🍓一维前缀和模板: ✨二维前缀和 🍓二位前缀和模板: 前言:算法学习笔记记录日常分享,需要的看哈O(∩_∩)O&a…...
【Linux系统基础快速入门详解】Linux下安装软件必知必会4种方法(yum,编译安装,rpm包,二进制方式)等详解
在 Linux 下安装软件有多种方法可供选择,常用的包括 yum、编译安装、rpm 包和二进制方式。下面对这些方法进行详细说明: 使用 yum 安装软件yum 是 Red Hat 系列 Linux 发行版中常用的软件包管理工具,通过 yum 可以方便地安装、升级和删除软件包。yum 默认从官方仓库中下载软…...

ASEMI代理长电可控硅BT136参数,BT136规格,BT136说明
编辑-Z 长电可控硅BT136参数: 型号:BT136 RMS通态电流IT(RMS):6A 非重复浪涌峰值导通电流ITSM:25A 峰值栅极电流IGM:2A 平均栅极功耗PG(AV):0.5W 存储接点温度范围Tstg:-40 to 150℃ 工…...

代码线程安全
线程生命周期 synchronized synchronized会自动释放锁 synchronized同步代码块 synchronized后面括号里obj是锁对象(保证唯一);static修饰的obj对象是自定义MyThread线程类的静态成员变量,该自定义线程类所有实例共享保证锁对象唯一性;另一…...

Filebeat技术栈总结
filebeat 是一个轻量型日志采集器,本质上是一个 agent 。不依赖于任何应用,可以安装在任何节点上,可单独使用 Filebeat 并根据配置读取对应位置的日志进行上报和搜集。 filebeat 内置了常用的 output 组件,例如 kafka、ElasticSe…...
【App自动化测试】(十六)健壮性测试工具——Android Monkey
目录 1. 介绍2. 安装3. Monkey的使用4. money常用命令5. 常用事件类型参数6. Monkey使用参考 1. 介绍 Monkey是一个在模拟器或设备上运行的程序,用于生成用户事件的伪随机流。 为什么要使用Monkey这个自动化遍历工具? Monkey解决了一个测试痛点ÿ…...

实现第一个内核程序的Hello World
背景 在内核的开发中,总要先入个门。那么就要来编写第一个内核程序 入门 一个 module_init 程序是Linux内核模块的一部分,通过module_init 方法就能将程序载入内核。 module_init 方法需要以下步骤 编写module_init 的代码,并将其保存为…...

python基于协同过滤推荐算法的电影观后感推荐管理系统的设计
本课题所设计的影单管理系统,使用B/S架构,Python语言进行开发,它的优点代码不能从浏览器查看,保密性非常好,比其他的影单管理更具安全性。Python还容易修改和调试,毕竟影视是在不断发展过程中,难…...

Vue——状态管理库Pinia
写在前面:本文参考小满大牛的pinia专栏 一、Vuex与Pinia Vuex 和 Pinia 均是 Vue.js 的状态管理库,它们为 Vue 应用程序提供了一种集中式的、可预测的状态管理解决方案。 Vuex 是 Vue.js 官方推荐的状态管理库之一。它的核心概念包括 state、mutation…...

Linux:忘记root密码解决办法
如果你是虚拟机只要将光盘镜像连接到虚拟机上,以光盘iso镜像启动 如果你是真机或服务器那将实体u盘或实体光盘连接至设备并且以连接的设备启动 开机时候打断开机 使用 (u盘|光盘)引导启动 troubleshooting rescue a centos system 输入 1…...
Dockerfile(4) - RUN 指令详解
RUN 运行命令 shell 形式 命令在 shell 中运行Linux 上默认为 /bin/sh -cWindows 上 cmd /S /C RUN <command> exec 形式 RUN ["executable", "param1", "param2"] 必须双引号,不能是单引号 两种写法的实际栗子 RUN …...

一个完整的APP定制开发流程是怎样的?
随着移动互联网的发展,越来越多的 APP应用软件进入人们的生活,让我们的生活更便捷、更舒适。而随着互联网技术的进步,移动互联网应用软件开发行业也越来越成熟,为了适应市场需求,各种功能强大、性能良好的 APP应用软件…...

【数据结构】24王道考研笔记——线性表
线性表 目录 线性表定义和基本操作顺序表静态顺序表动态顺序表 链表单链表不带头结点:带头结点: 双链表循环链表循环单链表:循环双链表: 静态链表 顺序表链表比较逻辑结构:存储结构:基本操作: 定…...

【Linux C】基于树莓派/香橙派的蓝牙服务端——支持多蓝牙设备接入
一、需求 在树莓派/香橙派上利用开发板自带的蓝牙作为一个蓝牙服务端(主机),允许外来设备(从机)通过蓝牙接入进行通信,通信格式为透传方式;采用的编程语言为Linux C 二、环境准备 bluez安装 …...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...

渗透实战PortSwigger Labs指南:自定义标签XSS和SVG XSS利用
阻止除自定义标签之外的所有标签 先输入一些标签测试,说是全部标签都被禁了 除了自定义的 自定义<my-tag onmouseoveralert(xss)> <my-tag idx onfocusalert(document.cookie) tabindex1> onfocus 当元素获得焦点时(如通过点击或键盘导航&…...
shell脚本质数判断
shell脚本质数判断 shell输入一个正整数,判断是否为质数(素数)shell求1-100内的质数shell求给定数组输出其中的质数 shell输入一个正整数,判断是否为质数(素数) 思路: 1:1 2:1 2 3:1 2 3 4:1 2 3 4 5:1 2 3 4 5-------> 3:2 4:2 3 5:2 3…...
Python第七周作业
Python第七周作业 文章目录 Python第七周作业 1.使用open以只读模式打开文件data.txt,并逐行打印内容 2.使用pathlib模块获取当前脚本的绝对路径,并创建logs目录(若不存在) 3.递归遍历目录data,输出所有.csv文件的路径…...

多模态大语言模型arxiv论文略读(110)
CoVLA: Comprehensive Vision-Language-Action Dataset for Autonomous Driving ➡️ 论文标题:CoVLA: Comprehensive Vision-Language-Action Dataset for Autonomous Driving ➡️ 论文作者:Hidehisa Arai, Keita Miwa, Kento Sasaki, Yu Yamaguchi, …...

AI书签管理工具开发全记录(十八):书签导入导出
文章目录 AI书签管理工具开发全记录(十八):书签导入导出1.前言 📝2.书签结构分析 📖3.书签示例 📑4.书签文件结构定义描述 🔣4.1. 整体文档结构4.2. 核心元素类型4.3. 层级关系4.…...