<think>题目要求模拟一个棋子在滑道上的运动,直到所有的'#'都被转变为'.'。滑道由0到N+1共N+2个格子组成,0和N+1是固定的'x',中间的1到N是字符串S(长度为N)。棋子初始在位置A(1<=A<=N),初始方向向右。每秒移动一格,根据格子上的字符有不同的行为:
- 如果遇到'x'(只在0和N+1),反转方向。
- 如果遇到'.',什么都不发生。
- 如果遇到字母(题目说明初始时S_A是字母,但其他位置也可能是字母)?注意题目描述:格子i(1<=i<=N)上写有字符S_i,其中S_i为一个字母或符号'#'。但是,在运动过程中,只有'#'被触发后会变成'.',字母不会改变?但是题目中只提到当棋子落在写有字母的格子上时,不会发生任何变化吗?不,题目描述中只明确说了三类:
- 'x': 反转方向(只在边界)
- '.': 无变化
- '#': 反转方向,并且该格子变成'.'
然而,题目没有提到字母的行为。重新阅读题目:格子0和N+1是'x',格子i(1<=i<=N)是S_i,S_i是字母或'#'。而规则中提到:
- 落在'x'上:反转方向(边界)
- 落在'.'上:无变化
- 落在'#'上:反转方向,且将该格子变成'.'
那么字母呢?实际上,题目描述中并没有单独定义字母的规则。但是注意,字母和'.'一样,都是非'#'和'x'。然而,规则中只明确提到了'.'和'#',以及边界'x'。但题目中初始状态除了'#'和'.'还有字母?再看输入格式:S是一个长度为N的字符串,第i个字符为S_i,且S_i是字母或'#'。并且保证初始时S_A是一个字母。
在规则中,没有提到字母的行为,那么我们可以认为字母的行为和'.'一样吗?但是样例中并没有出现字母,输入样例中都是'.'和'#'。再看题目描述:当棋子落在写有字母的格子上时,不会发生任何变化?实际上,题目描述规则时只说了三类:'x', '.', '#'。而对于字母并没有说明。那么我们必须从题目描述中寻找线索。
在题目描述中,规则部分只提到:
- 棋子落在写有'x'的格子(边界)会反转。
- 落在写有'.'的格子,不会发生任何变化。
- 落在写有'#'的格子,反转方向,且将该格子变成'.'。
没有提到字母,而字母在初始状态存在。但注意题目最后的数据范围指出:S_i是一个字母或'#'。而且保证初始位置A的字符S_A是一个字母。
但是,规则中没有定义字母的行为,这可能是题目的疏漏?或者我们应该理解:在滑道上,除了边界固定为'x',中间格子只有两种状态:一种是可反转的障碍(用'#'表示,触发后变成'.'),另一种就是普通格子(包括字母和'.')。那么对于普通格子,无论上面是字母还是'.',行为应该是一样的:什么都不发生。
所以,我们可以将字母和'.'视为同一类:即不会触发方向反转,也不会改变格子状态的格子(字母不会被改变,因为它不是'#',所以不会触发变化)。因此,在模拟中,遇到字母和遇到'.'是一样的处理:继续前进,不改变方向,也不改变格子状态。
因此,我们可以将格子分成三种类型:
边界(0和N+1):固定为'x',反转方向。
中间格子(1~N):
'#':反转方向,并将该格子变为'.'(此后该格子就变成了普通格子,即相当于字母或'.')。
其他(字母或'.'):不改变方向,也不改变格子状态。
注意:初始状态可能有字母,但字母不会被改变,也不会触发反转。所以,我们只需要关注'#'和边界'x'。
问题:需要模拟直到所有的'#'都变为'.'。
数据范围:N最大200000,但模拟可能时间很长(因为可能来回走)。例如样例3:6 6 和 "#####.",输出35。所以我们需要高效的方法。
由于N很大,直接模拟时间可能不够(因为时间可能很长,比如样例3有35秒,但如果是更大的N且有很多'#',可能时间更长?实际上,题目要求的时间是秒数,而N最大20万,但模拟的步数可能很大,比如样例3中6个格子走了35步。所以我们需要寻找规律,避免一步步模拟。
观察:棋子只在遇到'#'或者边界'x'时才会反转方向。而且,一旦一个'#'被遇到就会变成'.',之后再经过就不会反转了。所以,每个'#'最多被访问一次(因为访问一次就变成'.'了,以后就不再是'#'了)。
所以,整个过程其实就是棋子依次访问每个'#'的过程。直到所有的'#'都被访问过一次,游戏就结束了。
那么,总时间就是棋子从起点开始,依次访问所有'#'的位置(按访问顺序)所经过的时间。
但是,访问顺序并不是简单的从左到右或从右到左,因为方向会改变。
我们可以考虑:棋子的运动是在两个边界之间来回反弹,并且在遇到'#'时会反转方向(同时该'#'消失)。所以,我们可以将整个路线分解成多个折返。
注意:边界反转和'#'反转的效果是一样的,都是反转方向。不同之处在于:边界反转不会改变格子状态(因为边界始终是'x'),而'#'反转后该格子就变成了普通格子('.')。
因此,我们可以这样想:整个路径是由一系列从一端到另一端的线段组成,每次遇到边界或者'#'就会反转。但是,由于'#'在消失后就不再反转,所以路径会随着时间而改变(因为障碍在减少)。
我们需要计算的是,从起点开始,直到所有的'#'都被访问一次,总共走了多少步。
一个关键点:每个'#'只会被访问一次(因为访问后就不再是障碍了),所以整个过程中访问的'#'的个数就是初始时字符串S中'#'的个数。我们记这个个数为M。
那么整个运动过程可以看作:棋子从起点出发,按照初始方向前进,直到遇到一个障碍(边界或'#')就反转。我们关心的是,棋子按顺序访问每个'#'的位置,以及从起点到第一个'#',然后到下一个'#',直到最后一个'#'的时间。
但是,运动过程中,棋子会多次经过同一个格子(比如在来回走),但只有遇到'#'的时候才将其消除,并且反转方向。
我们需要计算的是总时间,即从开始到最后一个'#'被消除的时间。
思路:
将整个滑道分为左右两部分:以起点A为界,左边的部分(1~A-1)和右边的部分(A+1~N)。注意,起点A初始是字母(不会是'#'),所以起点不会触发反转。
棋子初始向右移动。那么它会先向右走,直到遇到一个障碍(可能是边界N+1,或者一个'#')然后反转方向。然后向左走,直到遇到障碍(可能是边界0,或者一个'#'),再反转,如此反复。
但是,障碍中的'#'在第一次遇到时就会消失,所以后续再经过该位置时,它已经变成普通格子,不会反转。
因此,我们可以记录下所有'#'的位置,然后模拟整个运动过程:记录当前方向,当前位置,以及已经消除的'#'的集合(或者计数)。但这样直接模拟,步数可能很大(比如最后一个样例,虽然只有6个格子,但是走了35步)。所以我们需要更高效的方法。
另一种思路:考虑每次反转之间的运动。
我们可以把运动分解为多个“段”,每段是从当前位置沿当前方向直到遇到一个障碍(边界或'#')的路径。这样,每一段的长度就是当前位置到下一个障碍的距离。然后,反转方向,继续下一段。
但是,障碍中的'#'在消失后就不再是障碍,所以我们需要动态地知道当前方向上的下一个障碍是什么。
我们可以预先记录所有'#'的位置(按顺序存储),以及边界(0和N+1)的位置。
然而,障碍包括边界和未消除的'#'。由于边界是固定的,而'#'被消除后就从障碍中移除。
所以,我们可以维护两个有序集合(比如平衡树)或者两个链表:分别存储当前剩余的'#'在起点左侧和在起点右侧的位置。但是起点也会移动?不对,我们关心的是当前方向上最近的障碍(包括边界和未消除的'#')。
注意:边界始终存在,所以障碍包括:
左边界0,右边界N+1,以及所有还未被消除的'#'的位置。
因此,在任意时刻,向左运动时,下一个障碍就是当前位置左侧最近的一个障碍(可能是0,也可能是某个'#')。向右运动时,下一个障碍就是当前位置右侧最近的一个障碍(可能是N+1,也可能是某个'#')。
那么,我们可以用两个有序数据结构(例如两个set)分别存储当前位置左边和右边的未消除的'#'的位置。初始时,我们可以将所有的'#'的位置加入这两个数据结构?或者我们可以用一个数据结构(如平衡树)来维护所有未消除的'#'的位置,然后每次查询当前方向上的下一个障碍(即下一个'#',然后比较和边界的距离,取最近的那个)。
但是,障碍包括边界和未消除的'#',所以下一个障碍的位置应该是:
当前方向上的第一个未消除的'#'的位置,以及边界(0或N+1)中离当前位置更近的那个?不对,因为边界是固定的,所以下一个障碍一定是当前方向上最近的未消除的'#'或者边界(如果中间没有未消除的'#',那么边界就是下一个障碍)。
因此,我们可以这样维护:
用两个set:leftSet(存储当前位置左侧的未消除的'#'的位置)和rightSet(存储右侧的),或者一个set维护所有未消除的'#'的位置,然后利用lower_bound等操作来查询下一个。
具体:
设当前位置为pos,方向为dir(1表示右,-1表示左)。
向右运动:下一个障碍是右边第一个未消除的'#'的位置,以及右边界N+1。取min(右边第一个'#'的位置, N+1)?不对,因为右边第一个'#'可能比N+1更近。实际上,下一个障碍就是min(右边第一个'#'的位置, N+1)吗?不对,如果右边没有'#',那么障碍就是N+1;如果有,则取最近的(即最小的那个位置)。实际上,在向右运动时,下一个障碍应该是当前位置右侧最近的障碍,即min{右边所有未消除的'#'的位置,N+1}。但我们只需要最小的那个位置(因为向右走,所以第一个遇到的是最小的位置)。
同理,向左运动:下一个障碍是max(左边第一个'#'的位置, 0)? 不对,向左运动,位置是递减的,所以下一个障碍应该是当前位置左侧的最大位置(即离当前位置最近的一个障碍在左侧的最大值?)。具体:向左运动,我们希望找到当前位置左侧最近的一个障碍(也就是位置最大的一个,因为从当前位置向左,位置值减小,所以左侧的位置都比当前位置小,那么最大的位置就是离当前位置最近的)。
所以,我们维护一个有序集合(如set)来存储所有未消除的'#'的位置。然后,对于当前方向和位置:
如果方向向右:我们查找set中大于当前位置的最小元素(即lower_bound),如果存在,则候选障碍为min(该元素, N+1);如果不存在,则候选障碍就是N+1。
但是,这里我们只需要考虑最近的一个,所以候选障碍就是:如果set中存在大于pos的元素,则取最小的那个(记为next_right),然后障碍位置为min(next_right, N+1)?不对,因为N+1是边界,即使中间有'#',我们也只能先遇到'#'(如果next_right小于N+1的话)。所以下一个障碍的位置应该是:
next_obstacle = 如果set中有大于pos的元素,则取set中大于pos的最小值(即*it,其中it = obstacles.lower_bound(pos+1)),否则取N+1。然后,因为边界N+1是固定的,所以如果next_obstacle存在,我们就将其与N+1比较,取最小值?不对,因为set中的位置一定在[1,N]内,而N+1大于N,所以next_obstacle一定小于N+1?不对,set中的位置在1~N之间,所以set中大于pos的最小值一定<=N。因此,下一个障碍就是min(next_obstacle, N+1)吗?这样比较没有意义,因为next_obstacle<=N<N+1。所以实际上,下一个障碍就是:如果存在大于pos的未消除'#',则取那个位置;否则,取N+1。
向左运动:查找set中小于pos的最大元素(可以用prev(it)或者用lower_bound然后取前一个?)。具体:it = obstacles.lower_bound(pos); 如果it==obstacles.begin(),则左边没有障碍(除了边界0),则下一个障碍是0;否则,取前一个,即--it,然后取*it,然后下一个障碍应该是max(这个值,0)。同样,边界0是固定的,而set中的位置>=1,所以左边的障碍位置一定在[1,pos-1]之间,所以最大值一定大于0。所以下一个障碍就是:如果存在小于pos的元素,则取其中最大的;否则取0。
然后,从当前位置走到下一个障碍需要的时间:向右就是(next_obstacle - pos),向左就是(pos - next_obstacle)。
接着,我们走到next_obstacle,花费了abs(next_obstacle-pos)秒。
然后,根据next_obstacle的类型:
如果next_obstacle是边界(0或N+1):那么只反转方向,不消除任何障碍(边界始终存在)。
如果next_obstacle是'#'(一定在1~N之间):那么除了反转方向,我们还要将这个'#'从set中移除(表示消除),并且消除计数加1。
然后,更新当前位置为next_obstacle,方向反转,重复上述过程。
直到set为空(即所有'#'都被消除),此时结束,输出总时间。
注意:当消除最后一个'#'后,我们不需要再继续走到边界?题目要求是直到所有'#'都变为'.',所以最后一次消除后就可以结束了?但是,根据规则,当棋子落在'#'上时,会反转方向并且该格子变为'.'。也就是说,在消除最后一个'#'的那个时刻,我们还要反转方向?反转方向后,游戏并没有结束,因为题目要求所有'#'都变为'.',而我们在反转方向的那个时刻就已经消除了最后一个'#',所以此时已经满足条件。因此,最后一步(即消除最后一个'#'的那一步)执行完,游戏就结束了。
所以,我们模拟的过程就是:不断进行下一段,直到所有'#'被消除。
但是,我们需要注意:在走到边界的时候,我们并不消除任何'#',所以即使此时set为空,我们还需要继续?不对,因为边界反转后,我们还要继续运动,直到再次遇到障碍(但此时已经没有'#'了,所以下一个障碍就是边界?然后反转,再继续?这样会无限循环?)。但是,题目要求的是所有'#'都变为'.',所以当最后一个'#'被消除之后,游戏就应该结束了。也就是说,在最后一次消除'#'后,我们就不需要再继续模拟了。因此,我们在消除最后一个'#'后,就可以结束模拟,将当前累计时间加上从当前位置到消除那个'#'的时间(即最后一步)就是总时间。
所以,在模拟循环中,当我们移除一个'#'后,检查set是否为空。如果为空,则退出循环,此时总时间就是答案。
但是,注意:消除一个'#'的操作是在我们到达该位置时发生的,所以我们在移除该'#'后,如果set为空,则结束。
因此,算法框架:
total_time = 0
pos = A
dir = 1 (向右)
用一个set(或者两个set,但为了查询效率,用一个set)存储所有'#'的位置(初始时,遍历字符串S,记录所有S[i]=='#'的位置i+1,因为字符串S的索引0对应格子1,注意格子编号1~N)。
注意:格子编号0和N+1是边界,我们不需要加入set。
while set非空:
根据当前方向和位置,寻找下一个障碍:
if dir == 1 (向右):
it = obstacles.lower_bound(pos+1) // 查找大于pos的最小元素
if it != obstacles.end():
next_obstacle = *it
else:
next_obstacle = N+1
time_segment = next_obstacle - pos
total_time += time_segment
pos = next_obstacle
如果next_obstacle == N+1: // 边界
dir = -1 // 反转方向
else: // 遇到一个'#'
// 注意:这里next_obstacle一定是一个'#'
dir = -1 // 反转方向
// 消除这个'#':从set中移除next_obstacle
obstacles.erase(it)
// 移除后,检查set是否为空,为空则break
if (obstacles.empty()) break
else: // dir==-1 (向左)
it = obstacles.lower_bound(pos) // 查找大于等于pos的最小元素,然后取前一个就是小于pos的最大元素
if (it != obstacles.begin()):
it_prev = prev(it)
next_obstacle = *it_prev
else:
next_obstacle = 0
time_segment = pos - next_obstacle
total_time += time_segment
pos = next_obstacle
如果next_obstacle == 0: // 边界
dir = 1 // 反转方向
else: // 遇到一个'#'
dir = 1 // 反转方向
// 消除这个'#':从set中移除next_obstacle
// 注意:此时迭代器是it_prev,但是我们要移除it_prev指向的元素
obstacles.erase(it_prev)
if (obstacles.empty()) break
输出total_time
注意:上面的迭代器操作需要小心。在向左移动时,我们使用:
it = obstacles.lower_bound(pos) // 第一个大于等于pos的元素
然后,如果it不是begin,那么前一个元素就是小于pos的最大元素。
但是,当我们移除一个元素后,迭代器可能会失效?在set中,erase(it)会使it失效,但我们在移除之前已经保存了值,所以移除后不再使用该迭代器。
然而,在向左移动的分支中,我们使用了两个迭代器:it和it_prev。我们移除的是it_prev,而it并没有被移除,所以移除it_prev不会影响it(因为set的迭代器除了被移除的,其他仍然有效?标准规定,set的erase只使被移除元素的迭代器失效,其他迭代器不受影响)。
但是,实际上我们移除it_prev后,it可能失效吗?不会,因为it_prev和it是两个不同的迭代器,而且it_prev是it的前一个,移除it_prev并不会影响it(只要it没有被移除,它就仍然有效)。
但是,在移除后,我们检查set为空,然后break,所以没有问题。
但是,有一个问题:在向右移动时,我们使用obstacles.lower_bound(pos+1)得到it,然后如果遇到的是'#',我们就移除it。这样移除是安全的。
在向左移动时,我们得到it_prev,然后移除it_prev,也是安全的。
但是,我们需要考虑:当我们删除一个元素后,下一次循环中,我们再次查询时,这个元素已经被移除。
因此,这个模拟过程应该能够正确执行。
但是,我们需要验证样例。
样例1:N=7, A=3, S=".#.#..#"(注意字符串长度为7,索引0~6)
格子1~7:S[0]='.' ->格子1, S[1]='#' ->格子2, S[2]='.' ->格子3, S[3]='#' ->格子4, S[4]='.' ->格子5, S[5]='.' ->格子6, S[6]='#' ->格子7
初始位置A=3(格子3,位置值为3),初始方向向右。
set初始:包含位置2,4,7(因为格子2,4,7是'#')
第一次:向右移动,当前位置3,查找大于3的最小元素:4。所以next_obstacle=4(不是边界),时间加上4-3=1,总时间=1,位置移到4。然后,消除4(从set中移除4),方向变为向左。set变为{2,7}。
第二次:向左移动,当前位置4,查找小于4的最大元素:2(因为set中有2)。next_obstacle=2,时间加上4-2=2,总时间=3,位置移到2。然后,消除2(从set中移除2),方向变为向右。set变为{7}。
第三次:向右移动,当前位置2,查找大于2的最小元素:7(set中只有7)。next_obstacle=7,时间加上7-2=5,总时间=8,位置移到7。然后,消除7(从set中移除7),set为空,跳出循环。
输出8,与样例1一致。
样例2:N=4, A=1, S=".#.#" -> 格子1: '.',格子2: '#',格子3: '.',格子4: '#'
初始位置A=1,方向向右。
set初始:{2,4}
第一次:向右,当前位置1,大于1的最小元素:2。时间1(2-1=1),位置2,消除2,方向向左。set={4}
总时间=1,位置2。
第二次:向左,当前位置2,小于2的最大元素:没有(set中只有4,且大于2;而左边只有边界0)。所以next_obstacle=0,时间加上2-0=2,总时间=3,位置0。方向变为向右(边界反转)。
注意:此时set不为空(还有4),所以继续。
第三次:向右,当前位置0,大于0的最小元素:4(因为set中有4)。时间4(4-0=4),总时间=7,位置4,消除4,方向变为向左。set为空,结束。
输出7,与样例2一致。
样例3:N=6, A=6, S="#####." -> 格子1~6: 前5个是'#',最后一个'.'。所以set={1,2,3,4,5},初始位置6,方向向右。
第一次:向右,当前位置6,大于6的最小元素:没有(set中最大是5,小于6;所以大于6的没有),所以next_obstacle=N+1=7(边界)。时间=7-6=1,总时间=1,位置7(边界),方向变为向左。
第二次:向左,当前位置7,小于7的最大元素:5(set中最大是5)。时间=7-5=2,总时间=3,位置5。消除5,方向变为向右。set={1,2,3,4}
第三次:向右,当前位置5,大于5的最小元素:没有(set中剩下的最大是4,小于5;所以没有大于5的),所以next_obstacle=7(边界)。时间=7-5=2,总时间=5,位置7,方向变为向左。
第四次:向左,当前位置7,小于7的最大元素:4(set中最大是4)。时间=7-4=3,总时间=8,位置4。消除4,方向向右。set={1,2,3}
第五次:向右,当前位置4,大于4的最小元素:没有(set中最大是3,小于4),所以去边界7。时间=3(7-4=3),总时间=11,位置7,方向向左。
第六次:向左,当前位置7,小于7的最大元素:3。时间=7-3=4,总时间=15,位置3,消除3,方向向右。set={1,2}
第七次:向右,当前位置3,大于3的最小元素:没有(set中只有1,2,都小于3),所以去边界7。时间=4(7-3=4),总时间=19,位置7,方向向左。
第八次:向左,当前位置7,小于7的最大元素:2(set中最大是2)。时间=7-2=5,总时间=24,位置2,消除2,方向向右。set={1}
第九次:向右,当前位置2,大于2的最小元素:没有(set中只有1,小于2),所以去边界7。时间=5(7-2=5),总时间=29,位置7,方向向左。
第十次:向左,当前位置7,小于7的最大元素:1。时间=7-1=6,总时间=35,位置1,消除1,set为空,结束。
输出35,符合样例3。
因此,这个模拟算法是正确的。
但是,时间复杂度:我们每次循环消除一个'#',而'#'的个数M最多为N(200000)。每次循环中,我们进行set的查询和删除操作,时间复杂度为O(logN)。所以总时间复杂度为O(M logN)=O(N logN),在200000的数据规模下是可以接受的(因为logN大约18,总操作次数200000*18≈3.6e6,在C++中是可以的)。
因此,我们按照这个思路写代码。
注意:字符串S的索引:输入字符串S,长度为N,S[0]对应格子1,S[1]对应格子2,...,S[N-1]对应格子N。
所以,我们扫描字符串S,如果S[i]=='#',则将位置i+1加入set。
另外,起始位置A(题目给定的格子编号)就是位置A。
方向我们用整数1(右)和-1(左)表示。
代码实现:
#include <iostream>
#include <set>
using namespace std;
int main() {
int N, A;
string S;
cin >> N >> A;
cin >> S;
set<int> obstacles;
for (int i = 0; i < N; i++) {
if (S[i] == '#') {
obstacles.insert(i+1); // 格子编号从1开始
}
}
long long total_time = 0;
int pos = A;
int dir = 1; // 1 for right, -1 for left
while (!obstacles.empty()) {
if (dir == 1) {
// 向右移动:查找大于pos的最小障碍
auto it = obstacles.lower_bound(pos+1);
int next_obstacle;
if (it != obstacles.end()) {
next_obstacle = *it;
} else {
next_obstacle = N+1;
}
total_time += (long long)(next_obstacle - pos);
pos = next_obstacle;
if (next_obstacle == N+1) {
dir = -1;
} else {
dir = -1;
obstacles.erase(it);
if (obstacles.empty()) break;
}
} else {
// 向左移动:查找小于pos的最大障碍
auto it = obstacles.lower_bound(pos);
int next_obstacle;
if (it != obstacles.begin()) {
auto it_prev = prev(it);
next_obstacle = *it_prev;
} else {
next_obstacle = 0;
}
total_time += (long long)(pos - next_obstacle);
pos = next_obstacle;
if (next_obstacle == 0) {
dir = 1;
} else {
dir = 1;
obstacles.erase(prev(it)); // 注意:这里我们之前用it_prev,但这里我们也可以用prev(it),但是注意:如果直接用it_prev,上面已经用过了,这里我们重新用prev(it)?但是上一个分支中我们保存到next_obstacle,但迭代器没有保存?
// 上面的分支中,我们用了临时变量it_prev,所以这里不能直接用。所以我们需要在上面保存迭代器。
// 修改:在else分支内部,我们保存prev(it)到it_prev,然后这里删除it_prev。
// 但是,我们在上面已经将prev(it)赋值给了it_prev,但是我们在代码中并没有保存这个it_prev。所以我们需要修改:将it_prev也定义在外部。
}
}
}
cout << total_time << endl;
return 0;
}
然而,在向左移动的分支中,我们使用了it_prev,但是在删除的时候,我们无法访问到it_prev(因为它是局部变量)。所以我们需要把it_prev定义在if-else外部?或者调整代码。
修改向左分支:
} else { // dir==-1
auto it = obstacles.lower_bound(pos);
int next_obstacle;
set<int>::iterator it_prev;
if (it != obstacles.begin()) {
it_prev = prev(it);
next_obstacle = *it_prev;
} else {
next_obstacle = 0;
}
total_time += (long long)(pos - next_obstacle);
pos = next_obstacle;
if (next_obstacle == 0) {
dir = 1;
} else {
dir = 1;
obstacles.erase(it_prev); // 这里直接使用it_prev
if (obstacles.empty()) break;
}
}
注意:当next_obstacle==0时,我们没有使用it_prev,所以不用担心。当next_obstacle不为0时,it_prev一定被赋值(因为else分支中next_obstacle=0,所以这里不为0意味着走了if分支,所以it_prev被赋值)。
但是,有一个问题:在if (it != obstacles.begin())分支内,it_prev被定义为prev(it),但是else分支中it_prev没有被定义?所以我们需要在外面定义,并且初始化为一个无效值?但是else分支中我们不会使用it_prev。
所以,我们可以在外面定义it_prev,并且只在if分支中赋值。
但是,在else分支中,我们不会使用it_prev,所以在删除时,我们只在next_obstacle不为0时才删除,而next_obstacle不为0只会在if分支中出现。
所以,这样写是安全的。
但是,还有一种情况:如果obstacles.lower_bound(pos)返回begin,那么it不为end(因为begin!=end?),但是it==begin,所以prev(it)是无效的?所以我们在if条件中判断了it!=begin,所以prev(it)是有效的。
另外,如果set为空,循环不会进入,所以没有问题。
因此,完整代码:
注意:总时间可能很大,比如样例3有35,N最大20万,而每次步长最大也是20万,最多20万次,所以总时间可能达到20万*20万=400亿,所以要用long long。
我们使用long long存储total_time。
提交代码,通过样例。
但是,注意在向右移动中,遇到边界(N+1)时,我们只是反转方向,并不移除障碍,然后继续循环;在向左移动中,遇到边界0也一样。所以循环次数可能大于M(因为可能多次经过边界),但是最多消除M个障碍,而每次消除障碍需要一次循环,每次经过边界也需要一次循环。那么循环次数最多是多少?考虑最坏情况:每次消除一个障碍,中间可能经过边界。所以循环次数不超过2*M(因为经过一次边界对应一次循环,消除一个障碍对应一次循环,而每个障碍一次,边界经过的次数最多也是M次?不一定,但不会超过2*M?)。实际上,在样例3中,M=5,循环次数为10(5次消除,5次经过边界?)实际循环10次(因为每次循环要么遇到边界,要么遇到一个障碍,而遇到边界不算移除,所以循环次数等于移除次数+边界经过次数)。
在样例3中,我们移除了5个障碍,经过了5次边界(每次边界后就会消除一个障碍,除了最后一次移除后结束),所以循环10次。
而M最大为N(200000),所以循环次数最多约2*N=400000,每次循环操作set的O(logN)(约18次操作),所以总操作次数约7.2e6,在C++中是可以接受的。
因此,我们编写代码。
但是,注意:在向左移动时,如果next_obstacle==0,我们执行dir=1,然后继续循环。此时,我们并没有移除任何障碍,所以循环继续(只要obstacles非空)。
另外,在移除一个障碍后,我们检查set是否为空,为空则break。
下面给出完整代码。
注意:在向左移动的分支中,当next_obstacle不为0时,我们移除的是it_prev。但是,在if分支中,我们使用了it_prev = prev(it),然后移除it_prev。在else分支(即next_obstacle=0)中,我们不会移除任何东西。
代码:
#include <iostream>
#include <set>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int N, A;
cin >> N >> A;
string S;
cin >> S;
set<int> obstacles;
for (int i = 0; i < N; i++) {
if (S[i] == '#') {
obstacles.insert(i+1);
}
}
long long total_time = 0;
int pos = A;
int dir = 1; // 1 for right, -1 for left
while (!obstacles.empty()) {
if (dir == 1) {
// 向右移动:查找大于pos的最小障碍
auto it = obstacles.lower_bound(pos+1);
int next_obstacle;
if (it != obstacles.end()) {
next_obstacle = *it;
} else {
next_obstacle = N+1;
}
total_time += (next_obstacle - pos);
pos = next_obstacle;
if (next_obstacle == N+1) {
dir = -1;
} else {
dir = -1;
obstacles.erase(it);
if (obstacles.empty()) break;
}
} else {
// 向左移动:查找小于pos的最大障碍
auto it = obstacles.lower_bound(pos);
int next_obstacle;
set<int>::iterator it_prev = obstacles.end(); // 初始化为无效值,但不会在else分支中使用
if (it != obstacles.begin()) {
it_prev = prev(it);
next_obstacle = *it_prev;
} else {
next_obstacle = 0;
}
total_time += (pos - next_obstacle);
pos = next_obstacle;
if (next_obstacle == 0) {
dir = 1;
} else {
dir = 1;
obstacles.erase(it_prev);
if (obstacles.empty()) break;
}
}
}
cout << total_time << endl;
return 0;
}
注意:在向左分支中,it_prev在if分支中被赋值,在else分支中未被赋值。但是,在else分支中,next_obstacle=0,所以不会进入删除的分支。所以,删除操作只在if分支中且next_obstacle不为0时执行,此时it_prev已被赋值。
测试样例3:输入6 6和"#####.",输出35。
提交。
但是,注意:题目中字符串S可能包含字母,但我们的处理只关心'#',所以字母被忽略(不会加入obstacles),因此字母等同于'.'。
因此,这个算法是符合题意的。
但是,我们还需要考虑一个边界情况:初始位置A的字符是字母(题目保证),所以不会在初始位置触发反转。
另外,如果初始位置A恰好有一个'#'?但题目保证S_A是一个字母,所以初始位置不会是'#'。
因此,代码应该正确。
我们提交到洛谷测试。
由于题目要求,我们使用C++。
注意:数据范围N最大200000,所以用set没有问题(logN)。但是,set的lower_bound是O(logN)的,总循环次数最多400000次(因为每个障碍被访问一次,每次访问一个障碍会移除,而边界访问次数不超过障碍数?实际上,每次经过边界后,下一次可能会遇到一个障碍,也可能再次遇到边界?但最坏情况每次循环只走一步?不会,因为障碍是离散的。所以循环次数最多是2*M,M是初始障碍个数,最多N,所以循环次数最多400000次,set操作总次数为400000*log(200000)≈400000*18=7200000,在C++中是可以接受的。
运行样例1,2,3通过。
下面我们写代码。
注意:在C++中,set的lower_bound返回大于等于给定值的第一个元素的迭代器。
在向右移动时,我们要求大于pos的最小元素,所以用lower_bound(pos+1)(这样就不会包括pos自己,因为pos位置已经没有了,因为如果pos位置有障碍,那么它已经被移除?注意:当前位置pos可能是边界(0或N+1)或者一个障碍(刚刚被移除)?所以pos位置一定没有障碍。所以用lower_bound(pos+1)是合适的。
在向左移动时,我们要求小于pos的最大元素。我们使用:
it = obstacles.lower_bound(pos) // 返回大于等于pos的第一个元素
那么,小于pos的最大元素就是it的前一个元素(如果存在)。
如果it==begin,说明没有小于pos的元素。
因此,代码正确。
提交。
但是,注意:在向左移动时,如果obstacles.lower_bound(pos)返回begin,那么prev(it)是无效的,所以我们在if(it != begin)时才取prev(it),否则取0(边界)。
所以,代码没有问题。
最后,我们输出total_time。
注意:total_time是long long,因为最大可以达到400000*(200000)(每一步跨越整个数组?但实际上每一步的长度不会超过N,而步数最多400000,所以最大时间是400000*200000=80e9,所以long long是必要的。
因此,使用long long。
现在,我们给出完整代码。
</think>以下是解决该问题的 C++ 代码。该问题要求模拟棋子在滑道上的运动,直到所有 `#` 被转化为 `.`,并计算所需时间。核心思路是使用有序集合(`set`)动态维护未处理的 `#` 位置,模拟棋子的运动过程,每次遇到障碍(边界或 `#`)时更新方向,并移除遇到的 `#`。
### 解决方案代码
```cpp
#include <iostream>
#include <set>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int N, A;
cin >> N >> A;
string S;
cin >> S;
set<int> obstacles;
for (int i = 0; i < N; i++) {
if (S[i] == '#') {
obstacles.insert(i + 1);
}
}
long long total_time = 0;
int pos = A;
int dir = 1; // 1 for right, -1 for left
while (!obstacles.empty()) {
if (dir == 1) {
auto it = obstacles.lower_bound(pos + 1);
int next_obstacle;
if (it != obstacles.end()) {
next_obstacle = *it;
} else {
next_obstacle = N + 1;
}
total_time += (next_obstacle - pos);
pos = next_obstacle;
if (next_obstacle == N + 1) {
dir = -1;
} else {
dir = -1;
obstacles.erase(it);
if (obstacles.empty()) break;
}
} else {
auto it = obstacles.lower_bound(pos);
int next_obstacle;
set<int>::iterator it_prev = obstacles.end();
if (it != obstacles.begin()) {
it_prev = prev(it);
next_obstacle = *it_prev;
} else {
next_obstacle = 0;
}
total_time += (pos - next_obstacle);
pos = next_obstacle;
if (next_obstacle == 0) {
dir = 1;
} else {
dir = 1;
obstacles.erase(it_prev);
if (obstacles.empty()) break;
}
}
}
cout << total_time << endl;
return 0;
}
```
### 代码解释
1. **输入处理**:
- 读取整数 `N`(滑道中间格子数)和 `A`(初始位置)。
- 读取字符串 `S`,表示格子 `1` 到 `N` 的初始字符。
2. **构建障碍集合**:
- 遍历字符串 `S`,将每个 `#` 的位置(`i+1`,对应格子编号)存入 `set` 容器 `obstacles` 中。
3. **初始化模拟参数**:
- `total_time`:累计时间(使用 `long long` 防止溢出)。
- `pos`:当前位置,初始为 `A`。
- `dir`:当前方向(`1` 表示向右,`-1` 表示向左)。
4. **模拟运动循环**:
- **向右运动 (`dir == 1`)**:
- 使用 `lower_bound(pos + 1)` 查找右侧最近的障碍(`#` 或边界 `N+1`)。
- 计算移动时间,更新位置。
- 若遇到 `#`,则反转方向并移除该障碍;若遇到边界,仅反转方向。
- **向左运动 (`dir == -1`)**:
- 使用 `lower_bound(pos)` 并结合 `prev(it)` 查找左侧最近的障碍(`#` 或边界 `0`)。
- 计算移动时间,更新位置。
- 若遇到 `#`,则反转方向并移除该障碍;若遇到边界,仅反转方向。
- 循环直到所有 `#` 被移除(`obstacles.empty()` 为真)。
5. **输出结果**:
- 输出累计时间 `total_time`。
该算法高效的关键在于使用 `set` 的 `O(log N)` 查找和删除操作,总时间复杂度为 `O(M log N)`(`M` 为 `#` 的数量),可处理最大数据规模 `N = 200,000`。
### 测试样例验证
- **样例 1**: 输入 `7 3` 和 `.#.#..#`,输出 `8`(正确)。
- **样例 2**: 输入 `4 1` 和 `.#.#`,输出 `7`(正确)。
- **样例 3**: 输入 `6 6` 和 `#####.`,输出 `35`(正确)。