竞赛讨论区 > 【题解】2024牛客寒假算法基础集训营2
头像
tokitsukaze
编辑于 02-06 00:35
+ 关注

【题解】2024牛客寒假算法基础集训营2

前言

赛中一位同学过了L没过K,这位同学的代码wa在了K的小数据,但由于牛客塞不下那么多组数据,那组数据没加在L和M中,出题人为此谢罪(悲)。过会儿会在L和M中放那组小数据。

然后请大家锐评一下本场比赛与题目,问卷链接:https://www.wjx.cn/vm/OowqS3N.aspx#

难度情况

出题时预估的难度:

通过率 (过题人数/有提交人数:4526) 预测情况: alt

主要偏差大的是:BIJK

B:主要是题意有一些不清,以及可能萌新对循环没那么上手(?)

IJ:似乎很多同学都被无向图和完全图的概念卡了,题目本身难度不大

K:前排大佬想直接冲LM,后排在跟榜,导致中后期才被大家发觉K不难

题目花絮

  • A:玩红烧天堂玩的。

  • B:本来是放去年寒假营的,后来删了,就挪到今年了。

  • C:同IJ,本来想放CF2D

  • D:玩游戏王玩的。idea 来源已经写在题面里了,就是在知乎上看见一个回答说用魔救拔刀。

  • EF:idea 源于 2023 ICPC 杭州站 K题,其中 easy 版本由智乃提出。

  • GH:"我想出一个有关线段树的数据结构题",想了好几天,突然翻到一个以前的idea "查询i在区间[L,R]中 sum(1,i)-sum(i+1,n) 最大",然后发现还能加强,于是就加强了一下变成这题。easy 版本原本想着:不带修答案不就是查询的区间 的和,减去 ?简单前缀和题.jpg 结果一写发现假了,样例都过不去,然后发现还是一个区间 RMQ 问题无法简化,干脆就带修了。

  • IJ:去年还是前年脑补出来的题,本来打算再出一场CF,两题合一放2C,后来怕协调员又是 74TrAkToR(我的上一场CF的协调员就是他,把我的题几乎毙光了,然后最近他又一堆负面新闻,所以...),而且也没有很多后补题,就不想出CF了,于是就把这两题放到寒假营了。

  • KL:CF1575D 超级加强版。去年某天打算出一场小白赛,准备把这题当小白F,然后在出的时候一不小心又被我狠狠的加强了一下,写完std后发现好像不适合放小白F,于是那场小白就被我鸽了(之后qcjj偶尔会拿我鸽小白这件事来diss我)。easy 版本目的是让萌新练练码力。

  • M:本来是没有这题的,赛前两天一个牛逼验题人说有更快的做法,然后我看了一下 hard 的代码,确实,于是就有了这题。

A. Tokitsukaze and Bracelet

签到题,按题意模拟即可。

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154445

B. Tokitsukaze and Cats

不考虑相邻,答案是

相邻会使答案变小,我们统计每只猫与几只猫相邻,然后把结果求和。

假设结果求和后的变量叫 ,于是答案为 。显然 是偶数可以直接除。

时间复杂度

当然,如果把 只猫的坐标都丢进 set 或者 map,然后枚举每只猫的 个方向判断是否有其他猫,时间复杂度可以做到

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154466

C. Tokitsukaze and Min-Max XOR

由于求的是子序列,套路的,我们可以对 进行排序。

排序后发现,当 时,中间数可以随便选,如果这组 min 与 max 满足条件,那么答案是 ,特别的,当 时,答案是

但这个做法既要枚举 min 又要枚举 max,是 的。遇到这种情况,优化思路大多都是枚举一个,快速查询另一个。由于条件是异或,我们考虑 01字典树 (01 Trie)。把比当前枚举的 小的数全部插入进 Trie 里,这样每次就能在 的时间复杂度内求出所有满足条件的 min 的信息。

此时又有新的问题:怎么用 Trie 维护答案信息呢?

对于一个 ,它贡献的答案为 。我们可以将这个式子拆掉: ,于是 就分离了。所以我们可以把 插入 。当 时,在 Trie 中查询 , ,此时贡献为

都可以预处理求出 ( 要用到逆元)。然后枚举 max,每次在 Trie 中查询,总时间复杂度为

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154476

D. Tokitsukaze and Slash Draw

Solution 1:dp

验题过程中发现有各种姿势的 dp,这里给大家举个例子。

表示在 条件下目标卡片向上移动 步的最小 cost 。对于每种操作分别枚举使用了 次该操作,卡片向上移动了 步时的 cost,由于在 条件下操作 次后卡片会一定会回到当前位置,所以操作次数 次不会更优,因此这个数组可以在 的时间复杂度内预处理出来。

然后就可以 dp 了。 表示最多移动了 步,当前位置在 的答案。转移只需要枚举初始在哪,然后通过初始的位置和移动了 步计算出移动后的位置,更新 dp 数组即可。最终答案为 :最多移动了 步,当前在位置

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154550

Solution 2:最短路

可以发现这题其实是一个起点为 终点为 意义下的最短路。

类似同余最短路 ,我们枚举余数 ,对于每个操作,连边 ,cost 为 ,然后直接跑 Dijkstra 即可。

此时如果使用堆优化的 Dijkstra,复杂度会多一个 (虽然也能过)。可以使用普通的暴力 Dijkstra,时间复杂度为 。由于建图的时间复杂度为 ,所以总时间复杂度与 dp 做法一致。

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154497

E. Tokitsukaze and Eliminate (easy)

考虑贪心,模拟每一步,只要每一步都能够消除最多的宝石,总操作次数一定最少。

easy 版本只有两种颜色,也就是说每次操作只有两种选择:要么选择的颜色与最后一个宝石的颜色相同,要么不同。显然,选择与最后一个宝石的颜色不同的颜色,每次能消除最多的宝石。

最后可能会出现"剩下的宝石都是同一种颜色"的情况,这时候选择另一种颜色也没办法消除,只能一个一个消除。

所以根据上面的思路,直接从后往前模拟即可。

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154573

F. Tokitsukaze and Eliminate (hard)

Solution 1:基于 easy 版本思路的拓展

hard 版本颜色不止两种,但根据 easy 版本的贪心思路,我们只需要维护每种颜色的最后一个宝石出现的位置,每次操作选择位置最靠前的那种颜色,这样一次就能消除尽可能多的宝石。

于是我们只需要模拟上述思路即可,模拟的方法有很多,这里介绍一种方法。

我们开 个vector 分别存下每种颜色的宝石下标。这些 vector 相当于是一个栈,其实也可以开 个stack,一样的,只是个人习惯喜欢用 vector。

第一次我们先遍历 种颜色,找到最靠前的下标。接着从后往前遍历宝石,遍历到的宝石一定在对应颜色 vector 的最末尾,消除它,就是把它从 vector 中 pop_back 掉。在这同时维护一个变量,表示此次遍历的宝石所对应的颜色最后一个宝石出现的位置。然后用这个变量进行下一次消除,直到所有宝石都被消除为止。

分析一下时间复杂度。每个位置只会进出一个 vector 各一次,每次消除每个宝石只会被遍历到一遍,所以总时间复杂度为

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154596

Solution 2:转化成经典贪心模型

考虑相同颜色的两个相邻宝石,前一个在位置 ,后一个在位置 ,颜色为 。显然,当位置 的宝石未被消除时,不可能选择颜色 来消除在位置 的宝石。如果我们想要选择颜色 来消除位置 之后的的宝石,当且仅当位置 的宝石已被消除。也就是说,此时末尾宝石的位置必须要在 中。

现在我们对每一个宝石都考虑这件事。 表示与第 个宝石颜色相同的下一个宝石的位置,那么消除第 个宝石与 之后的所有宝石,需要末尾宝石在位置在 中。

然后你会发现,这么做之后就把这道题转化成了一个经典贪心模型:线段覆盖问题。我们需要选出最少的线段来覆盖 。由于线段是 ,从前往后处理就能使线段有序,所以不需要排序,时间复杂度同样是

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154611

Solution 3:线段树优化dp

没错,你甚至还能写线段树优化dp!

表示消除 以及 之后的宝石的最小操作次数。根据 Solution 2,可以从 的 dp 值转移过来。

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154621

G. Tokitsukaze and Power Battle (easy)

显然是一个数据结构题,需要支持两种操作:

1.单点修改

2.区间查询,在查询的区间中选出一个子段,再把子段切成两部分,求前一个部分的和减去后一个部分的和的最大值。

对于查询操作,有两个问题:

  1. 怎么选子段?

  2. 把子段切成两部分,在哪切?

可以看到 easy 版本中数字都是非负数,稍加思考可以得到两个显然的贪心结论:

  1. 假设我们已经选出了一个子段 ,那答案必定为 ,也就是说 easy 版本省去了问题2 "在哪切"。

  2. 再多思考一下会发现,问题1 "怎么选" 也得到了简化。因为数字都是非负数,查询求的是前一段和-后一段和最大,所以前一段肯定尽可能长。那么对于查询的区间 来说,肯定选 这个子段,那答案就变成了 。也就是说只要确定了 ,就能得出答案。

根据上述两个结论,现在考虑如何求答案。

考虑维护区间中每个 的答案 。我们发现答案的一部分由区间和构成,所以我们先用前缀和对答案的式子做一些化简变形:

但这个式子还与 有关,无法直接用一个变量维护它的最大值,于是我们可以分开维护:

  1. 维护

  2. 维护

于是

注意此时单点修改 会对区间 中的 产生修改,所以其实是个区间修改。

我们可以使用线段树维护,支持 "区间加","区间求max" 即可。

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154633

H. Tokitsukaze and Power Battle (hard)

hard 版本跟 easy 版本比起来有了负数,easy 版本的两个贪心都无法使用了,此时无从下手。下面将介绍两种做法。

Solution 1:基于"区间查询最大子段和"思想

如果你熟悉线段树,做过"单点修改,查询区间的最大子段和" (洛谷P4513:小白逛公园),那么这道题对你来说应该并不难。

你会发现,这题查询的东西跟"区间查询最大子段和"就差了一个符号,如果把减号改成加号就完全一致了。于是我们可以考虑用线段树直接维护答案。

首先考虑答案的组成,按减号在哪段进行讨论:

  1. 左边包含右端点的最大答案(减号在这整段中)-右边包含左端点的最小子段和

  2. 左边包含右端点的最大子段和+中间整段(减号在这整段中)-右边包含左端点的最小子段和

  3. 左边包含右端点的最大子段和+右边包含左端点的最大答案(减号在这整段中)

  4. 减号就是中间的减号:左边包含右端点的最大子段和-右边包含左端点的最小子段和

然后你发现其实可以上面第2种情况可以归类到第1种和第3种情况,因为:

  • 左边包含右端点的最大答案=左边包含右端点的最大子段和+中间整段

  • 右边包含左端点的最大答案=中间整段-右边包含左端点的最小子段和

所以我们需要维护:

  1. 区间和

  2. 包含区间右端点的最大子段和

  3. 包含区间左端点的最小子段和

  4. 包含区间左端点或者右端点的最大答案

  5. 包含整段的最大答案

  6. 区间答案

然后写个合并节点的函数用于 pushup 和 查询答案。

核心代码就是合并节点的函数 merge_node,其他部分就是输入和线段树模板。

alt

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154669

Solution 2:基于 easy 版本思路的拓展

根据 easy 版本的做法,我们同样先尝试对答案式子做变形,把答案式子用前缀和表示出来,假设答案区间为 ,切点为 ,前缀和为

( and )

显然,要使这个式子最大, 都要尽可能小, 要尽可能大。所以该怎么求呢?

其实,这玩意也可以使用线段树直接维护,我们需要维护:

  1. 区间前缀和 的最小值与最大值

  2. 的最大值

  3. 的最大值

  4. 区间答案

维护方法与 Solution 1 类似,可以参考下面的 merge_node 函数。

alt

不过此时我们维护的是前缀和 ,所以单点修改要变为区间修改,区间修改就必然需要打标记和下放标记。所以对于区间修改,我们需要思考两个问题:

  1. 标记与标记如何合并

  2. 标记与值如何合并

由于这个区间修改就是普通的区间加,标记与标记直接累加即可,我们需要分析标记与维护的这些值如何合并。

假设现在要让区间加上

  1. 对于最小值与最大值 ,将变为

  2. 对于 的最大值 ,发现 要变为 要变为 ,所以对于 总共多了一个 的贡献,要变为

  3. 同理,对于 的最大值 ,要变为

  4. 对于区间答案 ,它所表示的式子为 ,你会发现 的修改被抵消光了,所以区间答案不变。

标记与值如何合并的问题解决了之后,这题也基本做完了。最后还有一个细节就是,我们维护的是 ,这个需要对下标简单处理一下,可以参考代码中的 main 函数。

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154673

总结

Solution 1 的难点在于是否看出这道题是"区间查询最大子段和"的变形(其实我在出题的时候也没看出来,看到 emofunc 验题代码才发现的)。

Solution 2 的难点在于是否能发现 这玩意能够使用线段树直接维护。

总的来说这道题应该还算是比较有 edu 意义的,题解写的比较详细,希望大家都能学会。

I. Tokitsukaze and Short Path (plus)

拆开绝对值可以观察到:

中,,由于 是正整数,绕路一定会加上正的贡献,所以绕路肯定不优。因此任意两个顶点 , 的最短路就是

如何计算?我们先对 数组从小到大排序,然后枚举 当做当前的 ,计算 对答案的贡献。

答案为

求和的部分可以进行预处理,计算答案部分的时间复杂度为 。加上排序,总时间复杂度为

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154725

J. Tokitsukaze and Short Path (minus)

拆开绝对值可以观察到:

中,,考虑绕路的情况。假设绕一次路,肯定选 最小的那个点走,那么此时答案就变成 ;假设绕两次路,显然边权没有负数,所以必定会加上一个正贡献,一定不如只绕一次路来的优。因此任意两个顶点 , 的最短路径要么是 ,要么是 ,其中 满足 =

如何计算?同样的,我们先对 数组从小到大排序,然后枚举 当做当前的 ,计算 对答案的贡献。

计算时需要讨论绕不绕路。令 等于第一个 满足 ,答案为

求和的部分可以进行预处理,计算答案部分的时间复杂度为 。加上排序,总时间复杂度为

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154730

K. Tokitsukaze and Password (easy)

Solution 1:模拟

使用 5 个循环直接枚举 a,b,c,d,_,的值,然后开个 set 暴力判重复值,特别好写。

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154775

Solution 2:dfs

直接 dfs 即可。时间复杂度会比 Solution 1 低一些,但细节比较多。

不过这题也算是签到题,时限很宽,样例过了大概就过了。

PS:这题的答案不会超过 。题面中写对它取模是因为要使 easy 版本的题面与 hard 版本的题面保持一致。

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154794

L. Tokitsukaze and Password (hard)

本题是一道 shit,核心考点比较简单,就是细节一堆。

本题的核心是:只要末尾3位能被8整除,这个数就能被8整除。因为1000是8的倍数,那1000的倍数一定是8的倍数,只要除以1000的余数是8的倍数,那么整个数就是8的倍数。

现在我们对4个限制一条一条分析:

  1. "没有前导0"的限制十分好解决;

  2. 枚举1000内8的倍数,先把末尾3位确定,这样就解决了"8的倍数"的限制;

3.""的限制可以这样:从高位往低位枚举,假设当前位为 ,比 高的位 全部满足 。如果 ,比 低的位就可以随便填,此时计算贡献,接着令 ;如果 ,直接往后遍历;如果 ,直接不合法 break;

  1. "不同字母代表的数字一定不同" 这条限制只需要每次记录哪些字母已经确定代表什么数字,哪些数字已经被用过。

所以我们可以枚举1000以内8的倍数,代入末尾3位。然后从高位往低位枚举,根据 的大小关系计算答案。细节比较多,具体可以参考代码。

时间复杂度

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154815

M. Tokitsukaze and Password (hard)

本题是 shit 上加 shit。名副其实的 shit problem!

根据 hard 的做法,发现需要计算答案的只有:

  1. 下划线

  2. 未填的字母

  3. 的第一个位置

进而发现:未填的字母最多只有 个; 的第一个位置计算完后就直接 break 了;下划线计算贡献的式子可以合并。所以只需要合并下划线的计算,每次计算最多跳 次位置,就能大大降低计算答案的时间复杂度。

我们可以预处理出每种字母出现的位置,用一个 vector 数组 表示;以及每种字母填了之后不等于 的位置,用一个 vector 二维数组 表示。这一步需要时间复杂度 。接着预处理出下划线贡献的前缀和;对于区间查询,会发现贡献要扣除 之后的下划线个数的 的幂,所以还需要预处理 的幂的逆元。

计算答案与 hard 一样,先枚举1000内8的倍数,然后跳到那些需要特殊处理的位置计算。为了方便,代码中会跳到 ,未填的字母,以及最后3位,最多遍历14次,每次需要在之前预处理的 vector 中二分找最近需要遍历的位置。所以时间复杂度为

总时间复杂度为 ,大概在 级别。

代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=67154820

全部评论

(3) 回帖
加载中...
话题 回帖

等你来战

查看全部

热门推荐