Live2D

Solution Set - “一二行诗句相遇,十万颗恒星解体”

\[\mathbb{Defining~\LaTeX~macros\dots} \newcommand{\dom}[0]{\operatorname{dom}} \]

0.「集训队互测 2018」Fim4 ⭐

  暂时不知道正解, 也不知道兔写了依托啥玩意儿. (

  询问串离线建出 AC 自动机, 我们需要将母串在 AC 自动机上匹配, 最后在 fail 树上求子树和得到答案. 母串长度 \(5\times10^9\) 不算太长, 我们可能可以压位来节约一个常数级别的复杂度卡过.

  取步长 \(w\), 对于 AC 自动机上的每个结点, 处理其 \(2^w\) 种转移到达的结点, 匹配时只在 \(2^w\times k\) 步到达的结点打标记, 这样匹配过程就可以除上一个 \(w\). 此后, 我们需要对 AC 自动机上的结点释放标记, 类似搜索地实现可以做到单个结点 \(\mathcal O(2^w)\) 释放标记. 复杂度 \(\mathcal O(|S|2^w+n|S|/w)\), 主要瓶颈是前一个部分的空间, 这里取 \(w=10\) 可以卡过. 当然兔释放标记的部分写的真的是搜索, 如果把这个东西展开成循环应该会快上不少.

1.「ABC 294Ex」K-Coloring ⭐

  题如其名, \(k-\)coloring 板题, 这个问题的一个表现较优秀的写法是采用缩图策略:

  • 若图不连通, 递归求解.
  • 若存在叶子, 答案乘上 \(k-1\), 删掉叶子及其连边, 递归求解.
  • 若存在度数为 \(2\) 的点, 讨论其两侧结点颜色是否相等, 递归求解.
  • 若上述缩图策略无效, 用子集卷积暴力求答案.

  毛估复杂度, 当图无法收缩时, 每个结点的度数 \(\ge3\), 则结点数 \(\le\lceil 2m/3\rceil\). 因此, 暴力部分复杂度 \(\mathcal O(2^{2m/3}m^2)\), 容易看出其余部分不是瓶颈, 这样就得到了一个 \(\mathcal O(1.59^mm^2)\) 的算法. 当然, 通过更精细的分析, 可以得到更紧的界.

  不好写? 一个 sweet 的写法是, 先判连通性, 然后取度数最小的结点, 容斥其邻接边颜色取等情况. 这种方法的递归 "底层" (无法缩图的情况) 复杂度略高 (吗?), 但的确舒服很多.

2.「NOI Simu.」解码

rep (t, 0, cur - 1) {
    if (same(i, t, cur, stpn)
    && same(i, (t + 1) % 10, cur, stpn)) {
        return true;
    }
}

  你说得对, 但 \(t\)\(\textit{cur}\) 是第 \(k\) 位而不是第 \(i\) 位. 你真会写代码.

  第一个想法是二分答案, 那就一条路走到黑吧! 对于二分的答案 \(t\), 我们考虑检查是否存在一个 \(m\neq n\) 使得 \(n,m\) 的前 \(t\) 步对应相同的字符串.

  比较 key 的 observation 是, 设 \(n\)\(t\) 步内产生过字符变换的最高的位为 \(h\), 则 \(n,m\) 的前 \(h\) 位 (第 \(0\) 位至第 \(h-1\) 位) 必然相同. 我们从 \(h\) 往上枚举 \(n,m\) 第一个不同的位以及 \(m\) 的对应取值, 注意取值是越小越好, 这样可以尽量避免产生 "\(m\) 先进位, \(n\) 再进位" 的情况, 减少对字符表的要求. 如果构造过程中已经使得 \(m<n\) 那就一定有解了 — 取 \(m\) 的高位和 \(n\) 相同即可. 贪心判定, 最终复杂度是 \(\mathcal O(b^2\ell^2\log n)\), 其中 \(b=10\).

3.「NOI Simu.」图 ⭐

  差不多会了, 写出来的是:

if (sum[rt] != 2 && sum[rt] != clr[rt]) {
    rep (u, 1, n) if (u != rt && clr[u] == clr[rt]) {
        clr[u] = 2; // Stimulate an adaptation (always exits).
        for (int v = u; v != rt; v = fa[v]) {
            sum[v] = 0;
            for (auto [w, i]: tre[v]) if (w != fa[v]) sum[v] += ans[i];
            sum[v] %= 3, ans[fe[v]] = (clr[v] - sum[v] + 3) % 3;
        }
        // break;
    }
}

注释行没有. 乐.

  先判断掉 \(n=2\) 无解, 因为题目中给了很多限制 (三部图, 连通), 我们可以坚信剩下的情况都有解. 先来考虑二分图的情况. 图上构造的常见思路: 取一个生成树, 我们就在生成树上调节度数. 假设在一种二染色方案中, 点 \(u\) 的颜色是 \(c_u\in\{0,1\}\), 一个自然的想法是构造边权使得 \(u\) 的边权和 \(s_u=c_u\). 在树上, 我们可以轻松地调节好除根以外的结点, 如果此时根 \(r\) 非法, 怎么办呢? 注意我们只用了两种颜色, 接下来就需要引入 \(c=2\) 了. 我们取 \(u\neq r\)\(c_u=c_r\), 令 \(c_u\gets 2\), 对应调整 \(u\to r\) 这条路径, 此后 \(s_r\) 必然被改变, 不管变成什么都是一定合法的!

  好, 放到三部图上, 我们仍然尝试用树构造 \(s_u=c_u\). 与二部图不同的是, 我们需要用树外的边构造答案. 非二分图一定存在奇环, 奇环上的边交替 \(\pm1\) 就可以让特定的点 \(s_u\gets s_u+2\), 我们用某个奇环调整根结点即可.

  两部分复杂度都是 \(\mathcal O(n+m)\).

4.「NOI Simu.」表达式

  这个… 有点怪啊, 猜猜出题人的 motivation 好了.

  首先, 这个问题不弱于 2-SAT, 且 SAT 结构整出来的 DAG 可以等价地描述变量关系, 我们不妨就在图上思考. 所有量词都是 \(\exist\) 时, 可以导出原命题为真的必要条件:

  • \(\forall i\), \(x_i\)\(\bar x_i\) 不再同一个 SCC 中.

  需要研究的是全称量词, 我们可以枚举一下两个量词之间的位置关系:

  • \((\exist x_i)(\exist x_j)\), 就是上面的东西.
  • \((\forall x_i)(\forall x_j)\), 如果 \(x_i\Rightarrow x_j\), \(x_i\Rightarrow\bar x_j\), \(\bar x_i\Rightarrow x_j\), \(\bar x_i\Rightarrow\bar x_j\) 任意一者的原命题或逆命题为真, 则原命题为假. 特别地, \(i=j\) 时, 仍然有同样的约束.
  • \((\exist x_i)(\forall x_j)\), 如果 \(x_i\Leftrightarrow x_j\), … 任意一者为真, 则原命题为假. 虽然初始的说法不长这样, 但经过一些化简可以得到这个结果.
  • \((\forall x_i)(\exist x_j)\), 没有额外限制, \(x_i\) 已经在 \((\forall x_i)\) 约束过, \(x_i\) 确定之后, \(x_j\) 一定存在.

  最后, 我们惊讶地发现这些限制都很好判断, 甚至不需要求 DAG 可达性. \(\mathcal O(n+m)\) 结束.

5.「ULR #1」「UOJ #577」打击复读 ⭐

6.「UR #7」「UOJ #84」水题走四方 ⭐

  不管是暴力 DP 还是研究结论, 我们必然要先来约束这个超高自由度的移动操作. 可以发现, 我们可以构造方案使得其中一个人从来没有传送向另一个人, 我们把没有传送过的人叫做本体, 另一个人叫做喻体分身, 那么, 整个操作过程就是: 本体在一条树链上行走, 分身去处理所有的分支子树.

  进一步的, 本体和分身的一些行动可以并行: 当分身只需要处理一条链时, 本体就不需要站在原地传送分身, 而可以继续向下走. 当然, 取子树中最长的树链一定最优, 这给本体预留了最多的行动时间. 麻烦的是, "子树最长链" 需要排除本体走的链, 我们根本不知道这条链是什么呀!

  贪心不了了, 我们尝试 DP. 如果知道本体在并行行走结束时站的位置, 我们就能求出分身处理的子树, 继而得到答案. 因此, 设 \(f(u)\) 表示处理好了 \(u\) 子树外的所有点, 现在本体和分身都站在 \(u\) 上时, 处理剩余部分的最短时间. 我们枚举第一次并行结束时本体走到的结点 \(v\), 则 \(\operatorname{sub}(u)\setminus\operatorname{sub}(v)\) 中需要存在一个深度不小于 \(v\) 的点 (这样才能并行), 有转移

\[f(v)\overset{\min}{\gets}f(u)+s_u-s_v-(c_u-c_v)d_u, \]

其中 \(s_u\) 表示 \(u\) 子树内叶子深度和, \(c_u\) 表示 \(u\) 子树内叶子个数, \(d_u\) 表示 \(u\) 的深度. 当然, 当 \(v\)\(u\) 的唯一儿子时, 有非并行的转移:

\[f(v)\overset{\min}{\gets}f(u)+1. \]

那么 \(\min_{u\in\text{leaf}}\{f(u)\}\) 就是答案.

  DP 优化倒是好说 — 并行转移中, \(v\) 肯定只需要被最近的可转移的 \(u\) 更新. 我们只需要对每个点 \(u\) 求出这个祖先 \(a_u\) 即可. 仍然是按子树处理, 递归完子树后剩下未被求解的 \(a_u\) 组成链表, 合并时交叉更新就行. 复杂度 \(\mathcal O(n)\), 尽量不要引入递归.

7.「UR #8」「UOJ #118」赴京赶考

  横纵坐标独立, 距离是区间内连续段数量. 注意是在环而非链上行走. \(\mathcal O(n+q)\).

8.「UR #8」「UOJ #119」决战圆锥曲线

  圆锥曲线是断然不会算的, 我们不如找一点简单的支配对来剪枝, 例如, 若 \((i,y_i)\)\((j,y_j)\) 满足 \(i<j\)\(y_i\le y_j\), 则 \((i,y_i)\) 断然不可能参与答案贡献.

  等等, 序列随机, 这个后缀最大值的数量是 \(\mathcal O(\log n)\) 的! 我们直接线段树维护区间最值, 然后树上遍历出所有后缀最大值更新答案即可. 复杂度 \(\mathcal O(n+m_1\log n+m_2\log^2n)\), 题目中有 \(m_2\le10^5\).

9.「SDOI 2013」「洛谷 P3303」淘金

  最暴力的暴力就是最正解的正解, 不愧是你.

  显然横纵坐标独立, 设 \(g(i)=|\{j\in[1,n]\mid f(j)=i\}|\), 我们如果求出所有的 \(g\), 剩下的工作就是求 \(g(i)\times g(j)\) 的前 \(k\) 大, 这个倒好说, 拿堆随便算算就行.

  怎么求 \(g\) 呢? 注意到 \(g(x)\neq 0\)\(x\) 必然有 \(x=2^a\cdot3^b\cdot5^c\cdot7^d\), 毛估不过 \(10^5\), 跑出来差不多 \(10^4\), 直接用 std::map 记忆化所有状态跑数位 DP 就行. 设状态数为 \(S\), 复杂度为 \(\mathcal O(bS\log S+k\log k)\), 其中 \(b=10\).

10.「ZJOI 2019」「洛谷 P5327」语言 ⭐

  • Link & Submission.
  • 「A.树论-虚树」「A.启发式合并/分裂」

  路径交叠特别复杂, 跨子树根或者跨分治中心计数都不太方便, 我们可以考虑另一种计数策略: 对于每个点, 求出其能够开展贸易的点数.

  对于点 \(u\) 答案的求解, 我们只需要保留所有经过点 \(u\) 的路径, 求出所有这些路径端点构成的虚树, 虚树 (对应于原树上) 的大小就是答案. 注意答案本身之和虚树形态有关, 而这样的虚树可以通过树上差分删除/加入单点, 启发式合并点集来维护, 这道题就这样了. 复杂度 \(\mathcal O(m\log^2m)\).

11.「ZJOI 2019」「洛谷 P5328」浙江省选 ⭐

  注意到 \(m\) 很小, 所以我们的算法大抵是一层一层地大力找到能当第一的一次函数, 把它们删除, 如此迭代. 当然, 求能当第一名的函数并不复杂, 就是求这些函数构成的下凸包, 在计算几何的语境下可以说是半平面交, 不过全部是 \(k,b>0\) 的一次函数, 单调栈扫一扫就行.

  求出凸包后由于各个位置被已经去掉的函数的覆盖次数是不一样的, 我们需要拿已经被剥离的上层函数在当前凸包上二分出覆盖部分, 打差分标记, 最后扫描凸包查看凸包上每个函数的露出部分是否存在一个整点, 该整点只被覆盖了层数 \(-1\) 次, 如果存在这样的整点, 对应的函数就进队 (迫真) 了.

  复杂度 \(\mathcal O(nm\log n)\).

12.「UR #8」「UOJ #120」宿命多项式 ⭐

  尝试通过枚举系数来满足限制. 不妨来计数整系数降幂多项式 \(f(x)\), 使得 \(f(i)\in[0,c_i)\cap\mathbb N\), 不难发现这和原问题等价. 设

\[f(x)=\sum_{i=0}^na_ix^{\underline i}, \]

那么

\[0\le a_0+ka_1+k^{\underline 2}a_2+\cdots+k^{\underline k}a_k<c_k\quad(\forall k). \]

设前面的部分为 \(d_k\), 则

\[0\le d_k+k!a_k<c_i. \]

可见, 我们关心 \(d_k\bmod k!\) 的值. 这里比较 key 的转化是, 我们可以通过预先构造 \(a_k=(n-k)!p_k+q_k\) 来降低信息量. 假设我们枚举了 \(\{q_n\}\), 令 \(M_k=k!(n-k)!\)在这种构造下,

\[0\le d_k+k!((n-k)!p_k+q_k)<c_k\\ \Rightarrow\text{count}(p_k)=\begin{cases} c_k/M_k, & c_k\bmod M_k\le(c_k+d_k+k!q_k)\bmod M_k\\ c_k/M_k+1, & \text{otherwise} \end{cases}. \]

其中

\[d_k\equiv\sum_{i=0}^{k-1}k^{\underline i}((n-i)!p_i+q_i)\equiv\sum_{i=0}^{k-1}k^{\underline i}q_i\pmod{M_k}. \]

可见, 的确只需要 \(\{q_n\}\) 就能计算方案数了. 直接枚举的复杂度是 \(\mathcal O(1!\cdot 2!\cdot\cdots\cdot n!\cdot n)\).

  你妈的, 怎么还有 \(q\) 次询问啊?

  尝试把 \(n!\) 这层枚举去掉. 好消息是, \(k^{\underline 0}q_0=q_0\), 因此 \(q_0\)\(\cdot\bmod M_k\) 的影响是一个匀速的转圈, 总共只有 \(\mathcal O(2^n)\) 个发生取模的点, 在两个相邻取模点之间的答案贡献恒定, 这样就能做到单次 \(\mathcal O(1!\cdot 2!\cdot\cdots\cdot (n-1)!\cdot2^n)\) 啦! 不过毕竟还得排序, 所以可以若干个序列统一进行 \(n!\) 值域的桶排.


  赞叹一下现代编译器, 编译期求值展开所有预处理过程就不说了, 对小数组的循环迭代貌似会倍增地展开成顺序结构 (比如对 int ary[7] 的迭代, 会生成迭代 \(4,2,1\) 次的汇编, 然后判断跳转). 但不知道为什么, 我的提交的代码在 g++ 10.3 中开启 -O3 选项时会警告:

.../main.cpp:66:38: warning: iteration 7 invokes undefined behavior [-Waggressive-loop-optimizations]
   66 |     rep (j, 0, k) val[k] += dpwr[k][j] * rem[j];
      |                             ~~~~~~~~~^
.../main.cpp:5:52: note: within this loop
    5 | , l, r) for (int i = l, rep##i = r; i <= rep##i; ++i)
      |                                       ^

.../main.cpp:66:5: note: in expansion of macro ‘rep’
   66 |     rep (j, 0, k) val[k] += dpwr[k][j] * rem[j];
      |     ^~~

而且相同的警告重复了三次 (难道对应了上面的倍增次数? 阅读汇编代码发现这个循环似乎确实被倍增展开了). 不知道为什么会有这个警告, 逻辑上 \(j\) 也不可能 \(=7\) 啊.

13.「NOI Simu.」乐 ⭐

  乐, 复杂度感知对了也除不下去 \(\omega\).

  经过足够时间的试错之后可以发现这题不太能 \(\text{polylog}\) (实际上, 似乎可以归约矩乘证明), 我们不得不在 \(10^6\) 的震撼规模下考虑根号分治, 继而可以猜到一个类似 \(\mathcal O(n\sqrt{n/\omega})\) 的复杂度 — 我们只需要在某种块长上除掉一个 \(\omega\), 就可以平衡出正解.

  设阈值为 \(B\), 对于询问 \((s,t)\), 若 \(\deg(s)\le B\), 该询问最多在 \(\mathcal O(B)\) 种颜色的连通块中有意义, 我们可以 \(\mathcal O(nB)\) (\(n,m,q\) 同阶, 下同) 地查询它们.

  现在, 我们只需要讨论满足 \(\deg(s),\deg(t)>B\) 的询问. 注意这样的点只有 \(\mathcal O(n/B)\) 个, 我们称它们为关键点. 对每种颜色, 考虑其所有连通块, 若连通块大小不超过 \(B\), 我们还是可以 \(\mathcal O(B^2\cdot n/B)\) 地大力更新答案 (一共只有 \(\mathcal O(n^2/B^2)\) 个答案是我们关心的, 可以直接保存). 否则连通块大小大于 \(B\), 这样的连通块总共只有 \(\mathcal O(n/B)\) 个, 我们给这个连通块一个新的标号, 将其记录在其中的每个关键点上. 最后查询时, 我们需要额外加入这部分贡献, 即两个关键点的标号集合的交集大小, 用 std::bitset 维护即可. 这部分的复杂度是 \(\mathcal O(n^2/(B\omega))\).

  取 \(B=\sqrt{n/\omega}\), 平衡得到 \(\mathcal O(n\sqrt{n/\omega})\), 不过空间是 \(\mathcal O(n\omega)\) 的, 实际实现时阈值不能太小.

14.「NOI Simu.」铲雪 ⭐

  (这个星星纯粹是对代码量的反抗 w.)

  一眼丁真, 鉴定为道路铺设. 边上权值道路铺设的结论很简单, 我们知道

\[\textit{ans}=\sum_{u=1}^nw_u\max\{2x_u-s_u,s_u\bmod 2\}, \]

其中 \(s_u\)\(u\) 邻接边边权和, \(x_u\) 是其中的最大边权, 这个式子很好理解, 难点在于维护.

  当 \(2x_u\ge s_u\) 时, 我们定义此时 \(x_u\)\(\arg\max\) (之一) 为 \(u\) 的 dominator, 记为 \(\dom(u)\), 若不存在这样的点则 \(\dom(u)=0\). 注意到若一个点 \(u\) 的两条邻接边同时被 \(+d\), \(\dom(u)\) 只可能从非零值变为 \(0\), 若 \(u\) 只有一条邻接边被 \(+d\), \(\dom(u)\) 才可能从 \(0\) 变为非零值. 也就是说, \(\dom(u)\) 的全局变化次数是 \(\mathcal O(n)\) 的. 我们可以树剖剖分, 将 \(\dom(u)\) 非零, 且非父亲, 非重儿子的 \(u\) 拉到线段树上维护贡献和 \(2x_u-s_u\) 的最小值, 用于检测 \(\dom(u)\) 变化; 其他的 \(u\) 大力维护答案就行. 复杂度 \(\mathcal O(n\log n\log m)\).

15.「CF 1310C」Au Pont Rouge

  写一个 \(\mathcal O(n^2\log^2n)\) 甚至 \(\mathcal O(n^2\log n)\) 的玩意儿 (\(n,m\) 同阶), 全程暴力可过.

posted @ 2023-06-03 10:40  Rainybunny  阅读(148)  评论(0编辑  收藏  举报