cac / commander.js 提供了一个简单直接的 API 来创建命令行应用,你只需要指定用户应该如何使用你的程序参数,就能生成出一个相应的 CLI 应用,包含参数传递,选项解析,版本和帮助命令的自动生成等等。

我从零实现了一个使用类似 cac / commander.js 的 API 风格的 CLI 应用框架 —— Breadc,但是相比于 cac :

  • 添加了 TypeScript 类型推导
  • 移除了部分类型不友好的参数解析
  • 添加了多层 sub-commands 的实现

本文主要讲解 Bread 如何添加 TypeScript 类型推导,如何使用 Template Literal Type 进行类型体操。

阅读全文 »

题面

求模板串 $S$ 在 $T$ 中的出现次数。

其中 $1 \le |S|, |T| \le 10^7$.

时间限制 $5$ 秒, 内存限制 $\mathbf{1}$ MB, 且输入只能读取一次.

题解

KMP 模板题

题目让你在一个空间受限的背景下, 进行流式字符串匹配. 本题无论模板串还是询问串都无法放进内存, 你必须流式的一个一个读取.

首先, 回忆 Rabin-Karp 算法是怎么样的. 它对模板串 $S$ 求了一个 hash 值, 然后使用滑动窗口 维护询问串所有长度 $S$ 的子串的 hash 值, 这里时间复杂度是 $O(|T|)$ 的, 但是空间复杂度是 $O(|S|)$ 的 (需要存下整个窗口, 以供删除开头的字符). 显然无法满足本题的要求.

官方题解的做法:

  1. 读入模板串 $S$, 计算出长度为 $\lfloor \sqrt{n} \rfloor$ 的前缀 hash 值和完整串的 hash 值, 时间复杂度 $O(|S|)$, 空间复杂度 $O(1)$;
  2. 读入询问串 $T$, 维护 $\lfloor \sqrt{n} \rfloor$ 大小的滑动窗口, 可以求出询问串中所有和这个根号前缀匹配的位置. 相当于, 筛出去了一些必不可能匹配上的位置, 留下来一些位置需要求出相应的全长度的 hash 值.
    • 注意到, 直到这里实际上还没有本质的改善, 一是匹配上的位置仍然很多, 二是难以搞出相应的全串 hash 值.
    • 根据 (Weak) Periodicity Lemm, 我们可以把匹配上的位置分成至多 $\lceil \sqrt{n} \rceil$ 组等差数列. 具体的, 每组中所有匹配的子串是一个顺次有重叠的列表, 相邻出现位置的距离差相等 (也就是所谓的构成等差数列, 进一步, 重叠部分其实就是根号前缀的 border). 于是, 记录匹配位置的空间被压缩到了 $O(\lfloor \sqrt{n} \rfloor)$.
    • 下一个问题是, 我们不仅需要记录匹配的位置, 还需要记录匹配处的前缀 hash 值, 等到处理到该次可能匹配的结束位置时来进行全串的比对. 同样根据上述引理的结论, 同一组等差数列内部, 相邻 2 个匹配位置之间的 hash 值是固定的, 我们只需要记录等差数列的起点, 公差, 终点, 起点的 hash 值, 公差对应字符串部分的 hash 值, 就能表示出这一个等差数列的所有位置信息和 hash 值信息.

最终, 时间复杂度 $O(n)$, 空间复杂度 $O(\lfloor \sqrt{n} \rfloor)$, 一是维护滑动窗口, 二是维护所有等差数列.

阅读全文 »

题面

给定一个长度为 $5000$ 的正则表达式和一个长度为 $10000$ 的文本串,求文本串最少修改多少次能够被正则表达式识别。

题解

对正则表达式构造 NFA,令 $f(l,i)$ 表达式使用完前 $l$ 个字符后,到达了 NFA 的状态 $i$ 时的最小修改次数,转移方程的形式为求一个 $0/1$ 最短路。

XLex 正则表达式

阅读全文 »

题面

有一个长度为 $n$ 的未知数组,第 $i$ 个位置的值有有 $k_i$ 种选择,这个位置选择 $V(i,j)$ 的代价 $C(i,j)$。

当你确定数组后,有 $q$ 次询问 $[l, r]$ 区间内的最大值,这个数组的价值就是所有询问的和减去选数时的代价。

数据范围:$1 \le n \le 300, n \le \sum_{i=1}^n k_i \le 3 \cdot 10^5$。

分析

考虑区间 DP,令 $f(l, r)$ 表示确定完区间 $[l, r]$ 的价值。

预处理每个位置 $i$ 被区间 $[l, r]$ 内所有询问覆盖的次数。转移时可以枚举这个区间的某一个位置作为最大值,但是每一个位置有多种选择,这个位置不仅要在价值上比较优,还需要满足比上一层小的限制。

无视上面的区间 DP,我们对于一个点,可以发现这个点产生的贡献关于覆盖次数是一堆一次函数的 $\max$,也就是一个下凸壳。回到上面的 DP,我们在枚举最大值,使用预处理的覆盖次数,可以在凸壳上二分出对应的一次函数,得到最优价值。

其实,我们已经做完了,上面的算法已经能够得到最优解,且最优解一定是合法的。

简单证明一下,我们考虑点 $i$ 和点 $j$,假设点 $i$ 作为大区间时的被覆盖次数为 $x_1$,小区间 $x_2$,点 $j$ 类似地为 $x_3$ 和 $x_4$,显然 $x_1 > x_2, x_3 > x_4$。不妨设两个点 $i < j$ 分别只有两条线段 $f(x) = k_i x - b_i$ 和 $f(x) = k_j x - b_j$,且 $k_i > k_j$。因此,$i$ 必须作为大区间的最大值,而 $j$ 需要在内层。

合法解时的价值为 $k_i x_1 - b_i + k_j x_4 - b_j$,非法解时的价值为 $k_i x_2 - b_i + k_j x_3 - b_j$。

注意到 $x_1 - x_2 = x_3 - x_4$,因为这个差是所有区间左端点在 $i$ 左侧和右端点在 $j$ 右侧。

因此 $k_i (x_1 - x_2) > k_j (x_3 - x_4)$。

阅读全文 »

B Playlist

给定一个环形的数组 $a_1, a_2, \dots, a_n$,有一个指针 $p$ 在环上进行扫描,如果当前位置和上一个位置的数 $\gcd = 1$,那么删除当前位置,指针移到下一个位置重新开始,直到无法删除。

我们不能暴力模拟,因为每次可能会跳 $O(n)$ 次来找到目标删除的位置。例如:$2 \times 3, 3 \times 5, \dots, p$,每次会删除第一个数。

因此,我们需要优化这个找删除位置的过程。注意到,记 $\text{next}(i)$ 表示在剩下的位置中 $i$ 后面的那一个(环的意义下)。如果 $\gcd(a[i], a[\text{next}(i)]) > 1 $,那么这个 $i$ 位置和他后面的这一对数,在之后永远不会被当作一对删除,因此我们在找删除对的时候,不再需要考虑位置 $i$。因为,我们在不会跳过 $i$ 来删除 $i$ 后面那个,$i$ 后面那个会一直存在,直到 $i$ 被前面的删除,导致其变换前驱。

C Skyline Photo

给定一个排列 $h_1, h_2, \dots, h_n$,将这个数组划分为多个段,每个段的权值是 $h$ 的最小位置 $i$ 的权值 $b_i$,总权值是所有权值的和,求最大权值。

显然,如果权值都是正数那么只要分成长度为 $1$ 的段即可。考虑 $f(i)$ 表示以 $i$ 结尾的最大权值,那么这个权值会一直存活到下一个 $h$ 比它小的位置,这部分维护一个扫描线。然后考虑 $f(i)$ 是怎么得到的,会有两部分转移一部分是从上一个比它小的位置到这个位置的一段区间中取一个最大的,或者是从扫描线中取一个最大的。

D Useful Edges

给定一个带边权的无向图和 $q$ 组条件三元组 $(u,v,l)$。对于边 $(a,b)$,如果存在三元组 $(u,v,l)$ 和一条 $u \to v$ 的路径,满足这条路径权值和小于等于 $l$,并且 $(a,b)$ 在这条路径上,那么这条边是好边。求好边个数。

跑一下 Floyd 之后,对于一条权值为 $w$ 边 $(a,b)$,把条件列出来,存在 $1 \le i \le q$,满足 $dis(u_i,a)+w+dis(b,v_i) \le l_i$。注意到条件三元组会有 $O(n^2)$,显然不能全部枚举来判断。

移项 $w + dis(b,v_i) \le l_i - dis(u_i, a)$,这个式子的左边固定一个端点 $b$,我们枚举另外一个端点 $v_i$,然后希望预处理右边的最大值来直接判断。此时对于式子的右边就是对于固定的端点 $a$,在所有点 $1 \le u \le n$ 中,取最大的 $l(u,v) - dis(u, a)$。我们提取 $v$ 和 $a$ 作为键,$u$ 是唯一的变元,因此枚举 $u$ 预处理最大值,即可直接判断。

F Exam

给定 $n$ 个串,求有多对串 $(i,j)$ 满足 $s_i$ 是 $s_j$ 的子串,并且不存在 $k$ 使得 $s_i$ 是 $s_k$ 的子串,$s_k$ 是 $s_j$ 的子串。

首先,建出 AC 机,预处理每个点往上走第一个结束节点和 fail 树的 dfs 序。

我们枚举每一个串,计算它有多少个直接的子串。显然直接子串一定是,这个串所有前缀的最长后缀,满足这个后缀在字典中出现过,直接使用 AC 机预处理的信息容易得出(注意,完整串可能需要特判,因为完整串的最长后缀是其本身)。但是这些串中显然会有很多重复的,也会有很多串在另外的当中出现过。

我们结合 AC 机的结构,观察一下哪些串是重复的。显然每个最长后缀对应节点,在 fail 树上到根的路径都是出现过的,这些节点的串构成母串的所有子串。

但是,考虑下面这样的情况:

image.png

对于那个最长的绿串实际上被包含在蓝串内部,但是根据我们上述的做法,我们多统计了绿串。因为上述做法,只排除了后缀的情况,没有排除不是后缀而是内部的情况。

正确做法是,我们可以通过从右往左枚举右端点,记录单调下降的左端点,将所有左端点变化处作为询问的关键点,这样就能够排除串在内部的情况。

阅读全文 »

题面

给定一棵 $n$ 个点的树,每个点有权值 $a_i$ 和 $b_i$,定义 $f(x)$ 表示满足 $a_i$ 之和等于 $x$ 的所有树上独立集中,$b_i$ 权值和最大的方案数。你需要求出 $f(1), f(2), \dots, f(m)$ 的值。

其中 $1 \le n \le 50, 1 \le m \le 5000$。

分析

显然有一个 $f(i,j,0/1)$ 的 $O(nm^2)$ 的树形背包做法,无法通过。

我们考虑在 dfs 序上进行 DP,注意到一个点能否被选择只和它到根的链上的状态有关,于是我们按照 dfs 序枚举时,同时记录每种链上的状态的信息。但是,这样仍然难易通过,因为可能会存在一个长链。

对于树进行轻重链剖分,不同于传统的剖分后标号方式,我们首先标记轻子树的 dfs 序,然后标记重子树。对于这样的标号方式,当我们考虑 dfs 序上的第 $i$ 个转移到第 $i+1$ 个时,要么是沿着向下的边走了一步,要么是跳到该重链链头的父亲后,往下走了一步,或者重复往上跳链头的父亲,然后往下走一步。那么,以这样的方式标号后,每个点到根的路径上有用的点,就只有每条重链的底部节点(每条重链链头的父节点)。于是,我们对这些关键点进行状压。

标号和 dfs

转移时,需要考虑第 $i$ 个点上的关键点集合和第 $i+1$ 个点上的关键点集合的重复部分,得到具体的转移状态,分成使用节点 $i+1$ 和不使用节点 $i+1$ 两种进行转移。

阅读全文 »

2019 年 CCPC 秦皇岛 G Game on Chessboard

题面

给定一个 $12 \times 12$ 的正方形棋盘,放了黑白两种棋子,每个格子有权值 $w(i,j)$。有两种操作,删除一个棋子 $(i,j)$,花费为 $w(i,j)$,删除一对黑白棋子 $(i_1,j_1)$ 和 $(i_2,j_2)$,并且满足这两个棋子的左下角范围内不能有其它棋子,花费为 $|w(i_1,j_1)-w(i_2,j_2)|$。求删除所有棋子的最小花费。

分析

注意到,每次能够删除的棋子在一条从坐上到右下的轮廓线上,对轮廓线进行状压,$0$ 表示向下,$1$ 表示向右。

轮廓线有 $2n \choose n$ 条,状压枚举时,找到拐角点进行转移。

时间复杂度 $O(n^2 {2n \choose n})$。

2020 年 ICPC 上海 F Fountains

题面

给定一个长度为 $9$ 的序列 $a_1, a_2, \dots, a_n$,求选出 $k=1,2,\dots,\frac{n(n+1)}{2}$ 个不同的子区间时,最大化 $\sum_{L \le R} \max_{i=1}^k [L \le l_i \le r_i \le R] w(l_i, r_i)$,其中 $w(l,r)=\sum_{i=l}^r a_i$。

分析

首先,我们将线段 $[l,r]$ 看成平面上的一个点 $(l,r)$,显然这个点左上角的点对应线段都包含线段 $[l,r]$。

对所有子区间线段按权值由大到小排序,考虑 $dp(i,j,S)$ 表示考虑到了前 $i$ 条线段,选出了 $j$ 个,状态 $S$ 内的所有区间都已经确定了包含的线段,显然 $S$ 是一个从左下角到右上角的轮廓线。当我们枚举下一个放进来的线段时,就和原有状态求一个并,因为排序的缘故,只需要更新没有更新过的点的答案,同时维护出新的轮廓线。

时间复杂度 $O((\frac{n(n+1)}{2})^2 {2n \choose n})$。

阅读全文 »