JOISC 2022 Day 1 - 4 全题解

标题党,事实是只有除了和 flightsdevice2 两道通信题之外的 \(10\) 道传统题。

所有题面、下发样例、Ranking 和我自己写的代码全部都在:

链接:https://pan.baidu.com/s/1YxqHtAI38EnBATcvaxyk5g?pwd=hyfl
提取码:hyfl
百度网盘,狗都不用

对你可能有用的链接:JOI Spring Camp 2022 Online Contest

Day 1

jail

​ 正向地记录这道题是怎么想到的。首先树上问题有一些一般的套路,可能会想到一些按子树搞,或者每次拿两条路径判一判之类的,但想想也知道不靠谱。然后手玩一下第二个样例,看一看 NO 是怎么来的,其实能隐约感觉到有 “成环” 的意思,这时候就能想到建有向图判强连通性或者 2-sat 之类,但是刚才说了每次拿两条路径是不行的,所以不能 2-sat。

​ 就算猜到了要 Tarjan 但是归纳不出来条件也是白瞎的,所以就先拿个链的情况来分析一下怎么回事。一看如果两个方向相反的交叉了肯定不行,如果方向相同的一个包含另一个也不行,但是如果方向相同的只是交叉没有包含(好像)暂时又没事。试图把这些条件搬到树上,感觉非常不充分,而且对方向相同的没啥约束力。

​ 直接考虑硬上,从有向边的意义的角度考虑可能会有什么样的边。其实从常理来考虑可能要建很多很多变量表示每条路径经过每个点的时间会比较充分,但这看起来太冗杂不可能是正解。所以干脆破罐破摔,只考虑路径与路径之间的约束关系,并且注意到这对应一个结论可以一次把一条路径移动完再移动下一条路径,这个结论看起来还算比较靠谱,就当它是真的。发现一条路径 \((s, t)\),如果有另一条路径经过了 \(s\),它就要比这条路径后移动;如果它经过了 \(t\),它就要比这条路径先移动。这个条件非常简洁优美,让人陡增信心。

​ 考虑在 DAG 上表示出这些边,只需要把这条路径代表的点连向路径上的 \(t\),把路径上的 \(s\) 都连向这条路径。这需要实现一个点向树上路径连边和一个区间向一个点连边,用数据结构来优化它,树剖之后相当于一个点向一个区间连边,用线段树优化建图就好了。

kyoto

​ 首先这道题有一个比较浅显的结论:只会用到在前缀 \(\min\) 或后缀 \(\min\) 处的 \(A_i, B_j\)。略证如下:考虑这样的一条路线

     B_1  B_2  B_3
A_1  ------|
A_2        |______

​ 考虑 \(A_1, A_2\) 中的较小者,如果 \(B_2 > B_1\)\(B_2 > B_3\),那么在走左下和走右上两条路线中一定有一条不劣于这条路线,就可以把 \(B_2\) 扔了。这个结论导出一个 40pts 的做法,因为有用的 \(A_i, B_j\) 个数是值域级别的。

​ 进一步考虑上面这张图更一般的情况,设五个值分别为 \(A_i, A_{i + x}\)\(B_{j - y_0}, B_j, B_{j + y_1}\),那么左下的路线和当前路线的差是 \(x(B_1 - B_2) - y_0(A_1 - A_2)\),右上路线和当前路线的差是 \(x(B_3 - B_2) - y_1(A_2 - A_1)\),只有这两者都大于等于 \(0\) 的时候 \(B_2\) 才有可能有用。它们分别是 \((B_1 - B_2, y_0) \times (A_1 - A_2, x)\)\(-(B_2 - B_3, y_1) \times (A_1 - A_2, x)\) 这就是说 \((B_1 - B_2, y_0)\) 要在 \((A_1 - A_2, x)\) 的顺时针,\((B_2 - B_3, y_1)\) 要在 \((A_1 - A_2, x)\) 的逆时针。又因为 \(x, y_0, y_1 > 0\),这三个向量在一个半平面内,所以 \((B_1 - B_2, y_0)\) 要在 \((B_2 - B_3, y_1)\) 的顺时针。所以 \((j, B_j)\) 必须在 \((j - y_0, B_{j - y_0}), (j + y_1, B_{j + y_1})\) 连线的下方,进而所有可用的 \((j, B_j)\) 都在下凸壳上,对 \(A\) 也同理。

​ 然后我们来考虑这两个凸包,注意到上面这两条折线相当于在 \(B\) 的凸包上的三个点 \((j - y_0, B_{j - y_0}) \to (j, B_j) \to (j + y_1, B_{y + y_1})\) 形成的两段折线当中插入了一个 \((i, A_i) \to (i + x, A_{i + x})\),就因为这段折线在(顺逆时针)的方向上位于那两段折线之间。所以最优解一定形如在两个凸包上走,每次挑选下一段折线方向更适宜的那个凸包走一个,而且这可以导出唯一的解,因此这么做就是最优的。而且因为现在 \(A\)\(B\) 都是凸包,所以一定能做到每一段折线都按顺序排,说白点就是凸包的闵可夫斯基和。

misspelling

​ 考虑一堆约束的本质,相当于钦定了 \(s[l, r - 1]\)\(s[l + 1, r]\) 的大小关系。进一步来说这就是要求 \(s[l]\) 开始的一段极长的相等字符之后接的第一个不相等的字符是比它更大或比它更小。字符集比较小,考虑 \(f(i, j)\) 表示 \(i\) 开始的后缀,\(s[i] = j\) 的方案数。考虑转移,如果下一个不和 \(s[i]\) 相等的位置在 \(k\),不妨设它比 \(j\) 大,那么所有左端点在 \([i, k)\) 之间的区间,要求比 \(s[i]\) 小的区间的右端点必须在 \(k\) 左边,也就是不能有一个区间穿过 \(k\)。随着 \(i\) 往左移动,相当于每次会把一段(从 \(i\) 开始的)前缀的 \(k\) 删掉。所以,用一个堆维护目前还合法的 \(k\) 就行了。

Day 2

copypaste3

​ 区间 dp,记 \(f(l, r)\) 表示这个区间的最小代价,手打对应的转移是 \(f(l, r) \to f(l, r + 1)\)。主要考察不断做剪切 - 粘贴这个操作有什么性质,发现它可以把 \(s\) 变成 \(A_1sA_2s\cdots sA_k\) 这种结构,其中 \(A_1, A_2, \cdots, A_k\) 是在两次粘贴之间手打进去的字符。首先我们考虑扔了 \(A_1\),记一个 \(g(l, r)\) 表示最后一次操作是粘贴的情形下的最小代价,那么 \(g(l, r)\) 可以往 \(g(l - 1, r)\) 转移,解决了 \(A_1\),这样 \(s\) 一定是从 \(l\) 开始的一个子串。首先如果选择粘贴 \(s\),我们一定会粘贴它的连续一段出现,中间不选择手打。注意到,因为 \(s\) 有长度,所以对一个 \(l\) 来说合法的 \((s, 粘贴次数)\) 对是调和级数的。那么如果选择的 \(s\) 的最后一次粘贴的右端点在 \(k\) 的位置,我们就可以转移到 \(g(l, r \geq k)\),并且代价关于 \(r\) 是一次项系数固定的一次函数(因为只有手打的代价 \(Cr\)),也就是我们用假设全部手打的代价减去所有 \(s\) 的长度之和,再加上复制粘贴这些 \(s\) 的代价。所以只希望常数项越小越好。打一个后缀标记再一遍前缀和过去就行了。这样就完成了 \(g\) 的转移,把 \(g\) 转移到 \(f\) 也是不费功夫的事。

flights

​ 通信题咕了。

team

​ 按照 \(x\) 扫描,那么另外两个 \(y, z\) 要构成逆序对,并且不得比 \(x\) 最大的这个的 \(y, z\) 小。我们在 \(x\) 排第二的这个人处把这个逆序对插入到数据结构里,那如果它要当 \(z\) 最大的,选的那个就肯定选 \(y\) 最大(但是 \(z\) 比它小)的。用两个 BIT 维护 \((y, z), (z, y)\) 的前缀最大值,用一个 BIT 套线段树维护 \((y, z)\) 逆序对的最大和。

Day 3

device2 (80pts)

​ 关键在于这样一个序列:\(B = [+1, -1, +1, -1, \cdots, +1, -1]\),它的前缀和是 \(+1 / 0\)

​ 如果考虑要传递的信息的前缀和,我们发现这个序列对前缀和只有 \(1\) 的扰动,那是否我们让要传递的信息前缀和全部 \(\times 2\) 就行了呢?这样我们可以知道只有和之前一个前缀和绝对值相差至少 \(2\) 的才是我们想要的,但是面临一个问题,举个例子上一次的前缀和是 \(2k\),这回的前缀和是 \(2k - 2\),那么这中间如果被扰动一下,得到 \(2k - 1\),也就是模 \(2\)\(0\) 和模 \(2\)\(1\) 都是原来的前缀和可能达到的值,我们分辨不出来。

​ 如果我们考虑让前缀和全部 \(\times 3\),就可以解决这个问题,因为 \(3k - 3\) 被扰动会变成 \(3k - 3, 3k - 2\),那么模 \(3\)\(2\) 是永远不会被达到的一个值,这样就可以分辨出来合法的前缀和。这样需要花费 \(180\) 个 bit。

sprinkler

​ 为了这玩意儿第一次写边分,也算是难忘经历。“点分 \(1\text{s}\) 就想到了对吧”(jt 语),然后发现因为不支持除法导致不能先用总的做再除以分治中心各个子树内部的;又不能前缀后缀扫描,那样要多带 \(\log\)。考虑边分,这样只有两个子树(也就是一般人们说的边分在度数相关问题上有优势),考虑相互之间的贡献,按照深度 two-pointers,用一个 \(40\) 的数组记录对应深度的贡献积就行了。边分之前要三度化(左儿子右兄弟就蛮不错的),虚边的长度需要设为 \(0\)

​ 但其实存在聪明而且低复杂度做法。考虑两点 \((u, v)\) 距离如果是一个和 \(d\) 奇偶性相同的数(而且比 \(d\) 小),那么一定存在一个公共祖先使得 \(u, v\) 分别到这个祖先距离之和恰好是 \(d\)(或者爬到根没祖先了),而奇偶性和 \(d\) 不同也同理一定存在一个祖先到两者距离和是 \(d - 1\),所以只需要在这些祖先的地方统计答案,每个祖先开个桶记录 \(40\) 个深度对应的个数就行了。

sugar

​ 我们实际上要求的是二分图的最大匹配,不妨考虑转化成求最大独立集。求最大独立集就意味着任意两个选择的 \(A, B\) 相距都要超过 \(L\),注意到这时候我们一定有如果选择了一个位置的 \(A\) 就一定选择这个位置的所有 \(A\)\(B\) 也一样,所以现在只关心每个位置选不选了。

​ 我们肯定会用一棵线段树来维护决策最大独立集的过程,但是如果决策每个位置选不选,就比较吃瘪,因为这样 dp 必然要涉及到记录上一个选择的位置之类的东西,是很不好维护的。于是我们考虑先选出所有的 \(A\),然后排除掉在这些 \(A\) 邻域内的所有的 \(B\). 不能选择的 \(B\) 是每个选中的 \(A\) 为中心半径为 \(L\) 的区间的并覆盖到的位置。

​ 接下来是比较骚的操作,如果有两个半径为 \(L\) 的这样的区间交了,那么我们不妨把中间交起来的地方里面的 \(A\) 也选上,这样徒增了 \(A\) 但是 \(B\) 没有变(因为长为 \(L\) 的区间的并没有变)。所以最终选择的 \(A\) 一定形如一堆连续区间,而且每个连续区间里 \(A\) 的半径为 \(L\) 的区间的并是不交的。这个并的右端点由这个连续区间里最右的 \(A\) 决定,左端点由连续区间里最左的 \(A\) 决定,这个连续区间设它最左的 \(A\) 选在 \(l\),最右的 \(A\) 选在 \(r\),它的贡献是:

\[\sum_{i = l}^r A_i - \sum_{i = l - L}^{r + L} B_i = \left(\sum_{i = 0}^r A_i - \sum_{i = 0}^{r + L} B_i\right) - \left(\sum_{i = 0}^{l - 1} A_i - \sum_{i = 0}^{l - L - 1} B_i\right) \]

​ 让两个括号分别是 \(f_r\)\(g_l\),所以我们相当于要选一堆 \(f\)\(g\) 交替使得和最大,\(l\) 可以等于 \(r\) 。这是一个比较方便用 dp 解决的问题,但是题目的修改操作来者不善。

​ 考察 \(A_i\) 单点加 \(x\) 会产生什么样的影响:

  • \(j \geq i\)\(f_j := f_j + x\)
  • \(j \geq i + 1\)\(g_j := g_j + x\)

​ 考察 \(B_i\) 单点加 \(x\) 会产生什么样的影响:

  • \(j \geq i - L\)\(f_j := f_j - x\)
  • \(j \geq i + L + 1\)\(g_j := g_j - x\)

​ 我们注意到对于 \(A_i\) 的操作,它等价于在 \([i + 1, n]\) 这段区间内对 \(f, g\) 同时区间加,在这个范围内它只跟 \(f, g\) 的个数差有关,而这个个数差只跟 dp 记录的端点选择的是 \(f\) 还是 \(g\) 有关(因为 \(f, g\) 是交替的);然后又在 \(i\) 这个位置对 \(f\) 单点加,这是容易处理的。

​ 然后再看 \(B_i\) 的操作,用类似的方法分析的话,相当于对 \([i + L + 1, n]\) 这段区间内对 \(f, g\) 同时区间加;对 \([i - L, i + L]\) 这段区间只对 \(f\) 加。这个就比较难受。因为选择的个数和 \(f\) 的个数相关。但是,我们已经说了对于 \([l_i, r_i]\)\([l_{i + 1}, r_{i + 1}]\)\(r_i + 2L < l_{i + 1}\),也就是说这段 \([i - L, i + L]\) 内的 \(l_i\) 就最多有一个了。

​ 现在我们记 \(dp(s, t, k)\) 表示这段区间内最左选中的是 \(s \in \{0, 1\}\),最右选中的是 \(t \in \{0, 1\}\),是否(\(k \in \{0, 1\}\))有选中至少一个 \(l_i\) 的情况下的最大值。

​ 这道题还有一个模拟网络流的做法,用霍尔定理(其实就是点数减最大独立集的特殊形式)可以判断是否存在完美匹配。二分图匹配增广有一个性质:加入新点时,已经在匹配中的点不会退出匹配,只会改变点之间匹配的方式。每次加入新点之后相当于想要让这些新点和已经存在的点中还没匹配的匹配,把它们钦定到匹配中去,并且使得被钦定的点集确实存在一个完美匹配,这样就代表新点确实可以和目前选择的这些匹配。处于贪心的角度考虑我们一定会试图让新点和能匹配的点中最近的匹配。然后考虑怎么判断一个点集是否存在完美匹配,其实就是对应上面做法中每一个选取 \(g, f, g, f, \cdots\) 的方案之和都要小于等于 \(0\)(对应霍尔定理的 \(|X_0| \geq \Gamma(X_0)\)),这等价于每一对选择 \((g, f)\) 的方案都小于等于 \(0\)。可以直接用线段树维护逆序对的方式来处理这个,转移的时候就看一看左儿子里 \(g\) 的最值和右儿子里 \(f\) 的最值。

Day 4

dango3

​ 一言难尽。。。考虑每次找出一串,一个好想法是每次二分(找一串复杂度 \(O(n (\log_2 nm))\))出一个颜色的最后一次出现(我们可以通过删掉一个前缀是否还能组成和全集组成个数一样的串来判断是否删掉了某个颜色的所有出现),然后它轻松地突破了上界。我把它和暴力结合,也就是剩下的数量比较少的时候,线性地找,这样需要五万八次。我考虑分块,每 \(B\) 个一块,如果这一块里有不合法的,就挨个找,否则跳过这个块,这样复杂度是 \(O(m(\dfrac{nm}B + nB))\),这样粗略分析当然是在 \(B = 5\) 的时候最优。但是实际上造组数据 \(B = 5\) 五万二,\(B = 9\) 四万七。。。然后交上去并没有通过,整了一组颜色分布比较整齐的数据,八万多。。。猛回头,把一开始的扫描顺序 shuffle 一下,就过了。

​ 一个据说被广泛采用的做法:扫描的时候维护若干不包含重复元素的串,加入的时候先从小的开始判断。

​ 一个真的很优异的做法:定义 \(\texttt{solve(S, m)}\) 表示你想的这个意思,然后扫描过程中维护一个每种颜色都不超过 \(\dfrac{m}{2}\) 次出现的极大集合,这样扫到头每种颜色应该恰好出现 \(\dfrac{m}{2}\) 次,分治下去。

fish2

​ 每个元素在吃掉一些相邻的元素之后权值会变成这段区间里元素的和,所以能吃掉的极长区间相等的元素是等价的。用线段树维护在当前这个区间内吃,每个极长区间对应的鱼的数量。注意到两点:第一点是我们只关心那些至少能吃到左端点或者右端点的区间;第二点是如果一个元素能吃掉一个前缀而恰无法吃掉下一个,说明这段前缀的和还没有下一个元素大,前缀和在这个位置就会翻倍,所以每个区间最多有 \(O(\log V)\) 个这样的区间。

​ 现在我们只关心怎么合并两个节点。我们注意到要合并的有这样一种单调性,即子节点较长的前缀在父节点里会变成的区间一定包含较短的前缀会变成的区间。但我们不希望直接愚蠢地模拟这个过程,因为我们不希望看到 \(O(\)区间长度\()\) 的合并。但是比如一个已经存在于右子节点的前缀,往左吃然后卡住,的事情也只会发生在前缀和翻倍的位置。所以我们不妨记录每个前缀和翻倍的位置对应的前缀有多少个元素能吃到这个前缀,然后每次都暴力判所有前缀和翻倍的位置,中间带一个 2-pointers 稍微优化一下。

​ 这样合并节点的时候就要关心前缀和翻倍的位置怎么合并,它显然是两边候选位置的并的一个子集,带着判一下就行了。

reconstruction

​ 这个问题就是边权是一次函数的最小生成树在 \(q\) 个点处的点值查询。能够感受到这个问题具有一定的凸性,每条边存在的时间是一个区间。我们现在来考虑怎么求出每条边存在时间的区间。先假设我们在一个询问时刻 \(t\) 非常小的时候,那我们相当于求的是最小生成树,那什么时候这个生成树会改变呢?其实就是如果有一条生成树外的边满足它比链上一条边的权值小了,这条边就会替换那条边。。。(这题解写成智障了属于是,“那什么时候生成树会改变呢?小编也不知道,小编也很好奇”)所以在链上最大的那条边和这条边权值的平均数的时刻,这条边会替换那条边。

​ 如果我们能找出这样替换的事件,按时间发生的顺序,那是再好不过,但这个比较难做到。如果把所有边按照从小到大排序,这会导致替换的事件不是按时间顺序被一一找到。好比说现在树上有两条边 \(1, 4\),用 \(5\) 在时刻 \(5.5\) 能替换 \(4\),用 \(6\)\(3.5\) 时刻能替换 \(1\),如果先加入 \(5\) 再加入 \(6\),那么时刻就乱了。但这实际上没关系,因为边的替换是这样点对点的,我们只要保证被替换的边永远是被最早替换它的替换掉就行了,而这正只需要用替换它的边的权值排序。

​ 由于 \(n\) 比较小,我们可以暴力维护这棵树,每次只需要暴力 dfs 找到链上最大边。然后要做的是一个对询问的区间加一次函数,用前缀和维护就行了。但是如果想偷懒,因为 \(n\) 比较小,所以每次遍历生成树上所有边也是可以的。。。

posted @ 2022-03-31 07:50  Mackerel_Pike  阅读(1544)  评论(0编辑  收藏  举报