SDOI 2024 考前做题
1. P9126 [USACO23FEB] Moo Route II S
首先注意到不一定保证 \(r_i\le s_i\),否则就是最短路裸题了。
注意到此时相当于负权图最短路。spfa 也许能过,但是我们想要复杂度正确的写法。
利用一下一条边出入时间固定(至少中途不会变)的性质:不难发现每条边最多只会走一次。不妨考虑 dfs,记录当前的位置和时间。考虑扩展,记当前的时间为 \(t\),则一条变能走需要满足 \(r\ge t\)。不难对每个点出边按 \(r\) 降序排序之后满足条件的是一段前缀。使用 vector 存图,每次记录一下当前弧,搜索即可。
注意当前弧要在 dfs 下去之后更新,不然如果出现环就寄了。可以参考代码。
时间复杂度 \(O(n\log n)\),瓶颈在排序。
2. P10199 [湖北省选模拟 2024] 时与风 / wind
是上题的加强版。
首先判断一条路径是否合法:显然充要条件是前一条边的 \([L,R]\) 被后一条边的 \([O,C]\) 包含。然后发现随机是诈骗,只需要取 \(R\) 就是答案。
还是考虑 dfs,不同的是记录的时间变成了区间。可以注意到数据范围有 \(L,O\le 20\),因此可以考虑对于每个 \(L\) 将以其作为左端点的 \(R\) 降序排序并维护当前弧即可。
复杂度不变。
3. P8289 [省选联考 2022] 预处理器
简单模拟。
首先 #define
和 #undef
是好处理的,只需要写一个函数判断标识符即可。在这个过程中用 umap
记录每个宏名被换成什么。
然后考虑普通文本的展开。首先找到每一段极长标识符,然后由于可能递归展开,写一个 dfs 模拟这个过程。整个过程和刚才的极其相似,先将其替换,并再用一个 umap
记录下其正在被展开,然后再遍历所有极长段递归展开。最后结束的时候再将其从 umap
中删掉。其他非标识符直接拼接即可。
需要注意的一点是,umap
删除元素不能直接 =0
(如果你判的是 .count()
),需要 erase
。
于是没了,你注意到 \(n\le 100\) 所以随便过。
4. P10060 [SNOI2024] 树 V 图
妙妙题。
首先进行一点简单的判无解:关键点的 \(f\) 显然是其本身在关键点集合中的编号,所以如果 \(f\) 数组没有取遍 \([1,k]\) 就是无解。
首先不难注意到一个事实:\(f\) 相同的点构成一个连通块。这是显然的,考虑反证。假设两个 \(f\) 相同的点中间有其他点,则中间的点一定还是离这个 \(f\) 更近。证毕。
由于是在树上,因此两个不同的连通块之间至多有一处相交,就是较深的连通块的顶部节点和其父亲。这个东西可以直接预处理。具体来说就是如果一条边两端颜色不同,就记录下交点。同时如果儿子的颜色之前访问过了,说明不是一个连通块,无解。注意这么做的话需要在一开始将根的颜色标记为已访问,不然会出问题。
然后我们通过简单的 dfs 求出树上节点两两之间的距离,复杂度 \(O(n^2)\)。
考虑两个连通块的交点,那么显然要满足:这两个点到各自关键点的距离相差不超过 \(1\)。至于到底是多一还是少一要根据关键点的编号判断。
于是可以考虑将连通块缩点,然后 dp:设 \(f_{u,i}\) 表示颜色 \(u\) 的连通块选 \(i\) 作为关键点,并满足子树内限制的方案数。答案就是新树上树根的 \(f\) 之和。注意到只有 \(u\) 联通块内的点的 dp 值有意义,转移就考虑枚举 \(u\) 选的 \(x\) 和儿子 \(v\) 选的 \(y\),那么 \(f_{u,x}=\prod_{v\in son_u}\sum_{f(y)=v}f_{v,y}\)。注意到每个 \(x\) 至多和 \(O(n)\) 个点一起被枚举,所以最终复杂度就是 \(O(n^2)\)。
5. Comet OJ Contest #14 E
有点意思的题,可以加深对 tarjan 代码 的理解。
首先一个环是可以随便走的,因此启发我们对 SCC 缩点。
但是我们发现缩点之后边权成了点权。这是不好的,所以考虑转化一下。
注意到对于点数 \(>1\) 的 SCC,内部是存在边的。不难发现我们只需要取出边权的 \(\max,\min\) 即可。建立三个点 \(l_i,mid_i,r_i\),然后连边 \(l_i\to mid_i\),边权为 \(\max\);连边 \(mid_i\to r_i\),边权为 \(\min\)。然后新图连边就直接 \(r_i\to l_j\) 即可。此时对于点数 \(=1\) 的 SCC,需要令 \(l_i=mid_i=r_i\)。
一些细节:在重建图的时候,若 \(scc_u=scc_v\),不难发现可以直接用 \(w\) 去更新所在 SCC 的边权 \(\max,\min\),就不用在 tarjan 的过程中去记录了,并且有重边也不会影响;若 \(scc_u\not=scc_v\),直接连边即可,不需要判重(这是我之前一直犯的一个错误,就是 tarjan 缩点后建图不需要判重,因为 topo sort 的时候一定会遍历每一条边,从而会将 \(v\) 入队),而且判重之后如果存在边权不同的重边也不好处理。
然后考虑 dp。设 \(f_{i,0/1/2}\) 表示从起点走到 \(i\) 的最大值/最小值/答案。初始 \(f_{i,0}=f_{i,2}=-\infty,f_{i,1}=+\infty\)(注意这里点已经没有点权了,只有边权)。
考虑转移。若存在出边 \(u\to v\),边权为 \(w\),那么先令 \(mx=\max(f_{u,0},w),mn=\min(f_{u,1},w)\),那么有转移:
这里有个小技巧:因为我们无法记录某条路径的 \(\max/\min\),所以可以直接钦定当前点为 \(\max/\min\),并更新答案。
无解:如果终点 \(=1\)/dp 值没有被更新/不在某个 SCC 里 则无解。为什么会存在不在 SCC 里的情况呢?因为我们是从 \(1\) 出发,所以 tarjan 的时候只遍历 \(1\) 所在的连通块。如果该点和 \(1\) 不在一个连通块则无解。
时间复杂度 \(O(n)\)。
6. P2824 [HEOI2016/TJOI2016] 排序
首先考虑序列只有 \(0/1\) 怎么做。
不难发现可以求出 \(0/1\) 的数量 \(c_0/c_1\),那么升序排序就相当于将前 \(c_0\) 个位置覆盖为 \(0\),后面的位置覆盖为 \(1\)。降序同理。
回归原问题,发现此时只需要求一个位置的值。考虑离线,二分答案,将 \(\le mid\) 的视作 \(0\),大于的视作 \(1\),然后直接做就行了。
7. MX【二月份 -- CSP-S 全真模拟】-- T3 --围堵
傻逼结论 && 诈骗题。
注意到题目规定(看样例)一开始所在的 \(1\) 不属于那 \(m\) 个点,于是初始令 \(dep_1=0\),求出每个点的 \(dep\)。令 \(dep=m\) 的点为关键点。
对于每个点,如果子树内存在 \(\ge 2\) 个关键点,那么必须要封住这个点。所以如果这种点数量 \(\ge m,\) 则无解(等于也不行,这样就能直接走过去了)。还有一种情况是子树内 \(>0\) 个关键点的情况,此时数量 \(>m\) 则无解(因为封的人是先手)。
于是没了,\(O(n)\)。出题人开 \(n,m\le 400\) 纯司马。
8. MX【二月份 -- CSP-S 全真模拟】-- T4 --畜牧
这才是好题。
首先有一个关键结论:一次只会卖一个点,而且全卖光。
考虑证明:只要我们可以把两个点的情况合并成一个点,就证完了。
考虑反证,假设在 \(i,j\) 处买了两次,\(i<j\),在 \(i\) 处买了 \(x\) 个。不难发现此时总价格是 \(xp_i+(\prod_{r=1}^i{k_r}-x)p_j\prod_{t=i+1}^{j}k_t\)。分成两种情况讨论:
- \(p_i>p_j\prod_{t=i+1}^{j}k_t\)
简单推一下:
考虑作差:
- \(p_i\le p_j\prod_{t=i+1}^{j}k_t\)
借用一下上面的结果:
得证。
接下来我们考虑 \(i\) 什么时候比 \(j\) 优:
注意到 \(p\) 只有 \(10^9\),所以直觉上 \(i,j\) 不会太远。
准确来说,对于一个 \(k\not=1\) 的位置,我们将后面 \(k=1\) 的一段和其合并起来,形成若干连续段。这样每个段内的 \(\prod k\) 都是一样的。那么 \(i,j\) 至多只隔着 \(\log V\) 段。
显然越靠后 \(\prod k\) 越大,所以我们只需要考虑最后面的 \(\log V\) 段。注意到每一段我们都要选 \(p\) 的最大值,可以使用线段树维护。比较就采用上文的方式,用一个 __int128
存储过程中 \(k\) 的乘积即可。当然最后算答案的时候还是要乘上前面的前缀积的,这个可以树状数组维护。
实现过程中,可以用 set 存储每个段的左端点,并用 vector 存储取出来的段。
时间复杂度 \(O(n\log^2 n)\),大概?
9. P9168 [省选联考 2023] 人员调度 48 分
是 \(O(nm\log^2 n)\) 的做法。
注意到我们可以从底向上考虑,这样子树内的点就是固定不可移动的了,然后考虑把当前节点的人向子树内调动。
具体来说,每个人维护三个 multiset
,分别存储初始在这个节点上的人(的能力值),子树内已经分配好的人,子树内空闲对答案没有贡献的人。分别记为 \(st,ok,gg\)。
注意到没有贡献的人显然是能力值最小的,因此可以这么设计算法:首先将所有儿子的 \(ok\) 和 \(gg\) merge 起来,然后考虑加入当前点的 \(st\):从大到小加入,如果当前 \(ok\) 没有满就加入,否则考虑用当前数替换 \(ok\) 的最小值,并将被淘汰的放入 \(gg\)。
注意到合并 multiset
的过程可以用启发式合并,每次直接 swap 当前点和重儿子的 multiset
(相当于直接清空重儿子的 multiset
),然后对于轻儿子和当前点的 \(st\) 再暴力插入。这样每个数只会被暴力插入 \(O(\log n)\) 次,每次插入 \(O(\log n)\),这样总复杂度就是 \(O(nm\log^2 n)\) 了。
复杂度稍微有点高,但是由于时限 5s 还是可以过的。
UPD;突然发现不用维护 \(gg\),破防了。
又 UPD:不难发现可以直接使用堆代替 multiset
。由于每次一个堆只需要查询最大/最小值,因此可以直接维护。删除就打下标记即可。
使用 pbds 的配对堆即可做到 \(O(nm\log n)\)。