ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java算法竞赛模板库:从快读、并查集到Dijkstra的实战避坑指南

Java算法竞赛模板库:从快读、并查集到Dijkstra的实战避坑指南 写算法题这事Java选手有多吃亏不用我多说。别人C队友敲完一行#include bits/stdc.h顺手就把并查集代码飞出去了我们还在逐字敲import java.io.*还得写public class Main那一整套样板。我自己打蓝桥杯和校赛的头两年就因为这个吃了不少哑巴亏。后来终于想明白了吃亏不在Java本身而在于我没把那些每次都要写的样板代码沉淀成一套自己的程序设计竞赛模板。与其每次现想现写不如提前把输入输出、常用数据结构、经典算法骨架整理成可以直接复制的Java模板。这篇文章就是来聊这件事的为什么要建模板库、模板应该怎么组织、我实际在用的核心模板代码长什么样以及蓝桥杯这类赛事里哪些坑必须提前避开。不管你是刚入坑的Java零基础选手还是已经写过几百道题的老手都可以直接把我这里的代码拿过去改一改变成你自己的东西。1. 为什么Java选手必须沉淀自己的竞赛模板1.1 模板不是抄代码而是把不变量沉淀下来很多人一听到模板就觉得是背题、抄板子听起来很不光彩。但换个角度想算法竞赛里真正区分水平的从来不是谁能默写Dijkstra而是谁能在有限时间内把思路落地成AC。我举个例子一道题的核心算法只占整个代码量的三成剩下七成全是输入解析、边界处理、输出格式化、数据结构初始化这些体力活。如果你每次遇到这些体力活都要现场debug那比赛后半程基本就是在跟自己的低级错误较劲而不是在跟题目较劲。模板的本质是把那些无论题目怎么变写法都不会变的部分固定下来。比如快读函数、树状数组的add和sum、并查集的find和union它们和具体题目的逻辑无关代码形态几乎恒定。把这些高频片段做成模板等于给自己的比赛代码加了一层保险丝只要模板是对的我只需要把注意力聚焦在算法思路上而不是反复检查i的边界是n还是n。还有一个很微妙的好处模板可以帮你统一编码习惯。我见过不少选手同一道题每次写的风格都不一样变量命名一会儿a一会儿arr导致自己看自己的代码都费劲。固定一套模板之后你的代码风格会高度一致比赛时排查问题会快很多。1.2 Java在算法竞赛里的优劣势决定了模板的设计方向坦白说Java在纯算法竞赛里是劣势选手运行速度比C慢起步代码更啰嗦递归深度还容易爆栈。但另一方面Java的API极其丰富处理大数、复杂字符串、复杂数据结构时非常省心而且Java的数组自带边界检查虽然慢一点但能拦住不少段错误。我遇到过好多次C选手在场上debug越界和指针问题我们用Java的反而跑完了。这种优劣势决定了Java模板的设计方向要快要省内存要能处理大数和不常见的数据结构。速度上必须避开Scanner和System.out.println这种生僻但巨慢的写法后面会细说内存上得警惕自动装箱——你随手写个ListInteger数据量一大直接MLE能力上BigInteger和BigDecimal在蓝桥杯的填空题和大数题里是救命的这套模板必须包含。所以我的模板库整体思路是核心骨架以快为主扩展模块以稳为主。比赛时先粘贴骨架再按需粘贴算法模块最后留出做修改的空间。1.3 我自己的模板库组织方式可以直接抄先说组织方式再说具体代码。我的模板库按照功能分成五个文件MainFastIO.java所有输入输出相关的模板包括快读类、快写、字符串Token解析。DataStructure.java树状数组、并查集、线段树、优先队列优化的图结构。GraphAlgo.javaDijkstra、Floyd、BFS/DFS、拓扑排序、最小生成树。MathUtil.java最大公约数、快速幂、质数筛、组合数、BigInteger的常用包装。StringUtil.java字符串哈希、KMP、正则简化写法、进制转换。每个文件里的模板都保持最小可用状态不搞花哨的泛型重载不写一堆用不上的重载方法。原因很简单模板越简单越容易背下来比赛时也越不容易写错如果你给每个方法都加八个重载比赛现场你可能会纠结到底用哪个反而拖慢节奏。2. 竞赛Java基础设施快读、排序与字符串处理模板2.1 快读快写模板解决TLE的第一块基石如果让我只带走一份模板去比赛必然是快读模板。Scanner的nextInt()慢到令人窒息因为它内部基于正则表达式去解析数据量一大就是你TLE的元凶。我自己做过一次实测一百万个整数的读取Scanner要花将近两秒而下面这种FastScanner只需要几百毫秒差距不是一点半点。import java.io.*; import java.util.*; public class Main { static class FastScanner { BufferedReader br; StringTokenizer st; public FastScanner() { br new BufferedReader(new InputStreamReader(System.in)); } String next() { while (st null || !st.hasMoreTokens()) { try { st new StringTokenizer(br.readLine()); } catch (IOException e) { e.printStackTrace(); } } return st.nextToken(); } int nextInt() { return Integer.parseInt(next()); } long nextLong() { return Long.parseLong(next()); } double nextDouble() { return Double.parseDouble(next()); } } public static void main(String[] args) { FastScanner fs new FastScanner(); PrintWriter out new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out))); int n fs.nextInt(); out.println(n); out.flush(); } }这里有几个容易踩的坑。第一StringTokenizer默认按空格、制表符、换行符切分如果题目数据有奇怪的字符需要传第二个参数new StringTokenizer(line, ,)来指定分隔符别用默认的硬啃。第二PrintWriter记得要flush()否则数据会留在缓冲区里导致输出不全但也不要每次输出都flush()比赛结束前统一flush()一次即可频繁flush反而拖慢速度。如果你懒得写类还有一个取巧方案用StreamTokenizer。它更接近C的C风格输入速度也快但处理方式比较别扭比如获取整数要用(int) st.nval获取字符串要注意TT_NUMBER和TT_EOF的分支不适合新手。我最推荐的还是上面这份FastScanner因为它简洁、可扩展也方便加nextLine()这类扩展方法。2.2 排序与集合类的正确姿势排序是竞赛里最简单也最容易被坑的主题之一。Java的Arrays.sort不是银弹得知道它的脾气对基本类型数组int[]、long[]用的是快速排序对对象数组Integer[]、Long[]用的是归并排序实际上是TimSort。这意味着如果你想对int[]做逆序排序直接写Arrays.sort(arr, (a,b) - b - a)是编译不过的因为基本类型不支持自定义要求传入的Comparator。正确做法有三种// 方案一先升序再原地逆序 Arrays.sort(arr); for (int i 0, j arr.length - 1; i j; i, j--) { int tmp arr[i]; arr[i] arr[j]; arr[j] tmp; } // 方案二转成对象数组再排冒泡/比较器都行 Integer[] boxed Arrays.stream(arr).boxed().toArray(Integer[]::new); Arrays.sort(boxed, (a, b) - b - a); // 方案三二维数组按某个维度排序 int[][] points new int[n][2]; Arrays.sort(points, (a, b) - Integer.compare(a[0], b[0]));注意第三个方案里的Integer.compare(a[0], b[0])不要图省事写a[0] - b[0]。当两个数异号且差超过Integer范围时减法的结果会溢出排序直接乱掉。这种bug特别隐蔽属于看着对但跑出来全是WA我从血泪里学的。还有如果数据量在百万级别以上尽量别用Arrays.stream(arr).boxed()这种方式装箱每个int变成Integer对象后内存膨胀好几倍MLE风险极高。至于集合类日常最爱用的是ArrayList、HashMap、PriorityQueue、ArrayDeque。这里有个小提醒如果涉及到频繁按下标访问get(i)LinkedList是绝对不要用的它每次访问都是从头遍历O(n)复杂度在循环里会变成O(n^2)。我见过太多人图方便用了LinkedList结果一道明明O(n)的题硬生生成TLE。2.3 字符串与数字处理模板蓝桥杯的题目尤其爱出数字和字符串混合处理的题比如让你判断一个字符串里哪些字符是数字、哪些是字母、哪些都不是。这种题如果纯手写ASCII判断非常啰嗦好在Java的正则和字符函数很齐全// 判断字符是不是数字/字母/空白 char c s.charAt(i); if (Character.isDigit(c)) { ... } if (Character.isLetter(c)) { ... } if (Character.isLetterOrDigit(c)) { ... } if (Character.isWhitespace(c)) { ... } // 常用正则注意Java字符串里的反斜杠要写成双反斜杠 boolean allDigit s.matches(\\d); boolean allLetter s.matches([A-Za-z]); boolean hasAlpha s.matches(.*[a-zA-Z].*); // 替换非数字字符 String cleaned s.replaceAll([^0-9], ); // 进制转换 String hex Integer.toHexString(255); // ff int decimal Integer.parseInt(ff, 16); // 255字符串拼接上循环里用StringBuilder不要用str ch。Java的String是不可变对象每次拼接都会创建新对象循环一万次就是一万次内存分配速度慢且容易触发GC。竞赛里养成习惯凡是循环里要拼字符串先StringBuilder sb new StringBuilder();最后再sb.toString()。大数处理也是高频刚需。如果题目里出现几千位的整数连long都扛不住必须用BigInteger。常用API就那么几个add、subtract、multiply、divide、mod、compareTo、gcd以及BigInteger.valueOf(long)和new BigInteger(String)。我强烈建议把BigInteger.ZERO、BigInteger.ONE、BigInteger.TEN这几个常量记牢配套BigDecimal时注意除法要指定精度.divide(b, 20, RoundingMode.HALF_UP)。3. 高频数据结构模板实战树状数组、并查集与线段树3.1 树状数组模板单点更新与区间查询、区间更新与单点查询树状数组Fenwick Tree是竞赛里性价比最高的数据结构代码短、常数小、比线段树好写十倍能处理的题目却覆盖了绝大多数点更新、区间查询和区间更新、点查询场景。我先把最基础的模板放上来这段几乎是每次比赛必用的。static class BIT { int[] tree; int n; BIT(int n) { this.n n; tree new int[n 1]; } void add(int idx, int val) { // idx 从 1 开始 for (int i idx; i n; i i (-i)) { tree[i] val; } } int sum(int idx) { int res 0; for (int i idx; i 0; i - i (-i)) { res tree[i]; } return res; } int rangeSum(int l, int r) { return sum(r) - sum(l - 1); } }核心就三行i (-i)取到当前节点的lowbiti lowbit是向上维护父节点i - lowbit是向下累加前缀和。为什么要下标从1开始因为如果从0开始i (-i)在0上会死循环。很多人第一次写树状数组都会栽在这里我建议直接在模板注释里写上索引必须从1开始。如果想处理区间更新、单点查询就换成差分思路把区间[l, r]同时加上val等价于在差分数组上执行add(l, val)和add(r1, -val)然后单点x的当前值就是差分数组的前缀和sum(x)。这个技巧太常用了比如循环数组的区间操作、统计前缀变化次数。我写了个示例// 区间[l, r]加 val查询点 x 的值 BIT diff new BIT(n 2); // 注意 n 要开大一点防止 r1 越界 diff.add(l, val); diff.add(r 1, -val); // 查询位置 x int value diff.sum(x);3.2 并查集模板路径压缩与按秩合并并查集是图论题里出镜率最高的数据结构没有之一。最小生成树、判环、连通块统计、离线处理都靠它。模板分递归和非递归两种写法我这里给的是递归版代码最简洁配合路径压缩和按秩合并单次操作近似O(1)阿姆斯特朗复杂度a(n) 是几不可再小的数量级。static class DSU { int[] parent, rank; DSU(int n) { parent new int[n 1]; rank new int[n 1]; for (int i 1; i n; i) parent[i] i; } int find(int x) { if (parent[x] ! x) { parent[x] find(parent[x]); // 路径压缩 } return parent[x]; } void union(int a, int b) { int ra find(a); int rb find(b); if (ra rb) return; // 按秩合并把矮树接到高树上 if (rank[ra] rank[rb]) { parent[ra] rb; } else if (rank[ra] rank[rb]) { parent[rb] ra; } else { parent[rb] ra; rank[ra]; } } boolean connected(int a, int b) { return find(a) find(b); } }这部分有两个细节值得展开。第一rank数组不是必须的只靠路径压缩也能跑得飞快。但如果你的数据里有大量把两个连通块并起来的操作不按秩合并的话树可能退化成链尽管路径压缩能救回来最坏情况还是会慢。我实际比赛时习惯加上按秩合并反正只多三行代码收益稳定。第二find里的路径压缩是递归的如果合并操作达到几十万次、递归深度也可能很深但实际因为压缩深度不会失控如果遇到栈溢出把find改成非递归版本即可——用循环先找到根再走一遍把路径上的父节点全部改成根。3.3 线段树模板区间和与区间最值的最小可用版树状数组虽好但有些题必须上线段树比如要区间最值、区间最大公约数、区间赋值和查询同时存在的时候。线段树代码长容易写错所以模板必须提前背熟。我给一个最精简的区间和 点更新版本static class SegTree { int[] tree; int n; SegTree(int n) { this.n n; tree new int[4 * n 5]; // 4n 空间是安全的 } void build(int node, int l, int r, int[] arr) { if (l r) { tree[node] arr[l]; return; } int mid (l r) 1; build(node * 2, l, mid, arr); build(node * 2 1, mid 1, r, arr); tree[node] tree[node * 2] tree[node * 2 1]; } void update(int node, int l, int r, int pos, int val) { if (l r) { tree[node] val; return; } int mid (l r) 1; if (pos mid) update(node * 2, l, mid, pos, val); else update(node * 2 1, mid 1, r, pos, val); tree[node] tree[node * 2] tree[node * 2 1]; } int query(int node, int l, int r, int ql, int qr) { if (ql l r qr) { return tree[node]; } int mid (l r) 1; int res 0; if (ql mid) res query(node * 2, l, mid, ql, qr); if (qr mid) res query(node * 2 1, mid 1, r, ql, qr); return res; } }有人会问区间和直接前缀和不好吗为什么非要线段树答案是前缀和只能处理静态数组一旦有点更新操作前缀和每次更新是O(n)线段树更新是O(log n)。所以只要看到题目里同时存在数组修改和区间查询就可以考虑线段树或树状数组。需要注意的坑是空间。树状数组只要n1线段树要用4 * n 5很多人这里开小了导致数组越界RE。如果只是点更新区间求和优先用树状数组不要为了练手硬上线段树比赛时间宝贵。4. 算法模板库二分、最短路与搜索骨架4.1 整数二分模板两种写法一次讲透二分答案在竞赛题里无处不在求最大值最小、最小值最大、单调序列查找、判断可行性。它的核心难点不在算法思想而在边界处理——写不好就是死循环。我习惯把二分为两种模板背下来// 模板一找第一个 target 的位置左边界的二分 int l 0, r n - 1; while (l r) { int mid l (r - l) / 2; if (arr[mid] target) { r mid; } else { l mid 1; } } // 循环结束后 l 就是要找的下标需要再判断 arr[l] 是否满足条件 // 模板二找最后一个 target 的位置右边界的二分 int l 0, r n - 1; while (l r) { int mid l (r - l 1) / 2; if (arr[mid] target) { l mid; } else { r mid - 1; } }你注意看两个模板的差异模板一里mid l (r - l) / 2模板二里mid l (r - l 1) / 2也就是上取整。为什么因为模板二里l mid这个分支如果配合下取整当l和r只差1时会卡在mid ll永远更新不了形成死循环。这是整数二分最常见的bug没有之一。我个人的经验比赛时遇到二分先确定我要求的是左边界还是右边界再决定用哪个模板、改一改判断条件不要边写边想。另外二分的判断函数check(mid)经常不是直接比较数组元素而是一种可行性检查比如return count(mid) k。这种题先把check写成独立方法返回boolean然后套模板能少很多逻辑混乱。4.2 Dijkstra堆优化模板优先队列的正确打开方式最短路是图论里的大热门。Dijkstra堆优化的核心就是用PriorityQueue来维护当前距离最小的节点。Java的PriorityQueue默认小顶堆但要注意存的是long[]还是自定义对象比较器要写好。我给一个我最常用的邻接表 long距离版本static Listint[][] graph; static long[] dist; static void dijkstra(int start, int n) { dist new long[n 1]; Arrays.fill(dist, Long.MAX_VALUE); dist[start] 0; PriorityQueuelong[] pq new PriorityQueue((a, b) - Long.compare(a[1], b[1])); pq.offer(new long[]{start, 0}); while (!pq.isEmpty()) { long[] cur pq.poll(); int u (int) cur[0]; long d cur[1]; // 懒删除如果这个节点的旧信息还在堆里直接跳过 if (d ! dist[u]) continue; for (int[] edge : graph[u]) { int v edge[0]; int w edge[1]; if (dist[u] w dist[v]) { dist[v] dist[u] w; pq.offer(new long[]{v, dist[v]}); } } } }这里有几个关键点。第一图用Listint[][]邻接表比邻接矩阵省内存得多。数据量大时邻接矩阵直接MLE邻接表才是王道矩阵唯一的好处是写起来快但打比赛我劝你别偷这个懒。第二PriorityQueue的remove(Object)操作是O(n)的不要用它来更新节点距离否则整个算法退化。正确做法就是代码里的懒删除堆里留下旧数据取出节点时判断d ! dist[u]就能识别垃圾数据。第三距离要用long而不是int我见过太多人 INF 只设成0x3f3f3f3f结果数据稍微开大点就溢出WA。如果要处理负权边得用SPFA或Bellman-FordJava下SPFA容易被卡数据能不用就不用如果图是稠密的且节点数在几百以内Floyd三重循环最省事代码就五行不解释了。4.3 DFS/BFS与回溯骨架搜索题最怕的是写错状态转移和忘记标记模板的作用是逼你把visited和边界条件先写好。我习惯把BFS写成队列 入队即标记的形式防止同一个节点入队多次DFS则写一个通用的dfs(int pos, int state, int depth)骨架// BFS 经典骨架 int[] dx {-1, 1, 0, 0}; int[] dy {0, 0, -1, 1}; boolean[][] visited new boolean[n][m]; ArrayDequeint[] queue new ArrayDeque(); queue.offer(new int[]{sx, sy}); visited[sx][sy] true; while (!queue.isEmpty()) { int[] cur queue.poll(); int x cur[0], y cur[1]; for (int i 0; i 4; i) { int nx x dx[i]; int ny y dy[i]; if (nx 0 || nx n || ny 0 || ny m) continue; if (visited[nx][ny]) continue; if (map[nx][ny] #) continue; // 障碍物 visited[nx][ny] true; // 入队即标记防止重复 queue.offer(new int[]{nx, ny}); } }这个骨架里最容易被忽略的就是入队即标记这六个字。如果你在出队时再标记visited同一个节点可能被多个邻居同时入队队列里堆一堆重复节点小数据看着没事数据一稍微大点就MLE或超时。另外ArrayDeque不要换成LinkedList的addLast/removeFirst虽然也能用但ArrayDeque不需要维护双向链表节点常数小很多。对于回溯类题目比如全排列、组合、子集骨架也差不多区别在于回溯恢复状态static void backtrack(ListInteger path, boolean[] used, int[] nums) { if (path.size() nums.length) { // 收集答案 return; } for (int i 0; i nums.length; i) { if (used[i]) continue; path.add(nums[i]); used[i] true; backtrack(path, used, nums); used[i] false; // 回溯核心 path.remove(path.size() - 1); } }递归深度是另一个隐患。Java默认栈大概能支撑几千层递归超过就StackOverflow。如果题目数据要求深度上万优先考虑把DFS改成显式栈BFS或者写循环版。我手里有一道 n100000 的树形题递归版必爆栈改成ArrayDeque模拟栈后稳稳AC。所以递归改栈这个技能Java选手必须掌握后面排查表里我还会提。5. 蓝桥杯实战中的模板运用与避坑清单5.1 蓝桥杯Java组特别容易踩的几个坑蓝桥杯和ACM不太一样它更偏向基础算法和大模拟而且Java组有自己的一套脾气。首先是最基础的类名必须是Main不能带package语句提交代码时不要选择带public class Test的模板否则直接编译错误。很多第一次参加蓝桥杯的人就栽在这上面当场心态崩。其次蓝桥杯的评测机和ACM类似对时间卡的也挺严格但选择题和填空题部分往往只需要输出结果不需要完整算法。这类题建议直接用System.out.println配合手工计算结果别写复杂算法去算效率反而低。我认识一个选手填空题老老实实写了个DP结果运行超时后来发现直接纸上算好填答案就行。大数问题是另一个坑。蓝桥杯历年都有涉及大数的填空题尤其是高精度计算。这时候别手写大数加法直接用BigInteger会更稳代码短还不容易错。但要注意BigInteger是对象每次运算都会新开对象如果循环百万次确实慢不过题目要求大数的数据量通常不会那么大实用优先。还有一点容易被忽略蓝桥杯很多题目的数据范围给的是10^18级别如果你用int读甚至连样例都可能过不了。我的习惯是只要看到数据范围超过10^9立刻用long超过10^18用BigInteger。这种条件反射要养成。5.2 模板的备份、定制与IDE配置模板不是写完就完事了得定期维护。我的做法是把所有模板扔到Git仓库里打比赛前checkout一份最新的平时刷题遇到好写的写法也会更新到模板中。这样做的好处是你永远只有一份权威版本不会出现本地三份模板互相冲突的情况。IDE层面我现在用IDEA打比赛。有两个配置强烈建议做一是把常用的代码片段录成Live Template比如psvm自动跳出public static void main(String[] args)模板sout自动变成System.out.printlnfastIO自动展开成我上面的FastScanner类二是配好自动导入import java.util.*和import java.io.*少写两行是一行。我自己的Live Template长这样快捷键是cpclass Main { public static void main(String[] args) { FastScanner fs new FastScanner(); PrintWriter out new PrintWriter(System.out); // 在这里开始写题 out.flush(); } }每次开新题先敲cp回车再往里填算法整套流程十秒内就能建立起一道题的基本框架。这就是模板的真谛不是限制你的思考而是帮你把无脑工作全部自动完成。5.3 常见问题排查速查表比赛中最怕的不是不会做而是写了半天代码疯狂报错。我把Java选手最常见的几类问题整理成一份速查表照着排查能省下不少时间现象可能原因排查与解决TLE 超时用了Scanner/System.out.println换成FastScannerStringBuilder批量输出TLE 超时循环内频繁String 拼接用StringBuilder代替TLE 超时递归深度过大、状态重复计算加记忆化或改用BFS/迭代MLE 内存超限大量使用Integer[]或ArrayListInteger优先用int[]基本类型数组减少装箱MLE 内存超限邻接矩阵存了 n^2 数据换邻接表或改用压缩存储RE 运行时错误数组越界检查下标是否从1开始、循环边界是否 nRE 运行时错误栈溢出把递归改成循环/显式栈或调大JVM栈RE 运行时错误除数为0检查模运算和除法代码WA 答案错误int 溢出把变量改成 long检查乘法/加法处是否越界WA 答案错误比较器减法溢出用Integer.compare/Long.compare代替a-bWA 答案错误二分死循环或边界错换模板二写法检查mid上下取整编译错误类名有public class Main之外的名字蓝桥杯一定写成public class Main删掉package这里面我特别想多说一句WA里面至少一半是int溢出。Java的int只有20多亿2^31-1而竞赛题的中间结果分分钟超过这个值。我在蓝桥杯做“连续子数组乘积超过K”这类题时就吃过这个亏——明明思路全对样例都过一提交WA最后发现product * cur早就溢出成负数了。从此养成习惯见到乘法、累加、前缀和先把类型定为long反正long占8个字节大部分题目内存都扛得住。6. 我的模板使用习惯与最后的经验分享说起来你可能不信我真正感到模板有用不是在比赛日而是在平时刷题的第三个月。那时我随便挑一道中等题从读题到AC大概要一两个小时现在有了固定模板写题的时间被压缩到二十分钟以内省出来的时间全花在理解题目和优化思路上。模板让我从写代码的人变成了解题的人。再给你一个我踩过坑之后总结的小办法每次打完一场模拟赛或正式比赛花十分钟复盘一下哪个环节因为没模板浪费了时间。比如上次我发现自己手写最大公约数用了五分钟就在模板里补上了gcd上次发现输出二维数组总是格式化出错就在模板里加了一个printMatrix(int[][] arr)方法。模板是活的谁都不可能一步到位关键是你愿意在复盘中迭代它。最后想说的是模板解决的是无脑重复,但真正决定你排名的永远是算法思维和数学直觉。所以别把模板当成偷懒的借口该刷题还是得刷题该啃《算法竞赛进阶指南》还是得啃。我的建议是把模板当成你手里那件趁手的兵器矿锄锋利一点挖矿自然快一点但矿藏在哪里、怎么挖通整条矿道还是得靠你自己的头脑。这个模板库我目前还在维护每次刷题遇到更精简的写法或者发现某个模板有边界漏洞都会及时更新。如果你刚入门我建议你先从我上面这五十多行基础模板开始一行一行的敲不要复制粘贴敲的过程中你会慢慢理解每一行的作用。等你真的理解透了再把树状数组、并查集、Dijkstra这些结构加进来。记住模板是你自己的不是别人的。别人背下来的模板到了赛场上你可能根本不敢用、不放心用只有自己敲过、调过、错过的模板才能成为你的肌肉记忆。
返回列表