#148. 花费(二分查找)

本文介绍了一个关于农场支出划分的问题,目标是最小化最大连续天数内的总支出。使用了二分查找的方法,通过设定开销的上下界,逐步逼近最优解。

【题目描述】:

农民约翰是一个惊人的会计,他意识到可能没有钱来经营农场。他已经准确计算出接下来的N 天,每天所要花费的钱ai。

约翰想将这N 天,划分成M 段,每段至少1天,第i段的和为sum[i]。

请你求出如何划分使得max{sum[i]}最小,其值是多少?

【输入描述】:

第一行两个整数N和M。

第二行N个整数,顺序表示每一天的花费。

【输出描述】:

输出划分M 段后,最大段的最小值。

【样例输入】:

7 5
200 300 300 200 500 221 420

【样例输出】:

500

【时间限制、数据范围及描述】:

时间:1s 空间:128M

1<=ai<=10,000; 1<=N<=100,000


详解看代码:

#include<stdio.h>
#include<iostream>

using namespace std;
const int N = 100005;
int n, m, l = 0, r = 0, mid = 0;
int a[N];

int read()

{
    int f = 1, ans = 0;
    char c;
    c = getchar();
    while(c < '0' || c > '9')
    {
        if(c == '-')
        {
            f = -1;
        }
        c = getchar();
    }
    while('0' <= c && c <= '9')
    {
        ans = ans * 10 + c - '0';
        c = getchar();
    }
    return ans * f;
}

bool IF(int mi)
{
    int sum = 0, t = 0;

    for(int i = 1; i <= n; i++)
    {
        if(sum + a[i] > mi)////如果前几个月加上这个月的钱大于这次尝试的月开销,那么说明这个月的钱应该放在下个月里 

        {
            sum = a[i];
            ++t;
        }
        else
            sum += a[i];
    }

    if(t >= m)
        return true;
    else
        return false;
}
 //如果按照mid的分割方法,最大分割出t个月大于m,则可以继续增加开销,
// 注意,这里是说,最少分出的月份是t,那么t>=m,则可以继续增加开销,以便使t变小 //本段注释为引用他人的话
//详见https://blog.youkuaiyun.com/abc15766228491/article/details/78827946

int main()
{
    freopen("51.in", "r", stdin);
    freopen("51.out", "w", stdout);
    n=read();
    m=read();
    for(int i = 1; i <= n; i++)
    {
        a[i]=read();
        l = max(a[i], l);
        r = r + a[i];
    }

    while(l <= r)
    {
        mid = (l + r) / 2;

        if(IF(mid))
            l = mid + 1;
        else
            r = mid - 1;

        mid = (l + r) / 2;
    }
    printf("%d", l);//一定是l而不是mid,因为l需要被最后一次修改才是正解,此时mid与l不是一个数,我调试了好久......
    return 0;
}
至于为什么做这题,,一方面是为了学习二分,,,,另一方面,,为了明天考试(要不然我才不那么勤奋呢^_^)................老天保佑我吧!!!嗷嗷!!
天下无双!(个人中二病)

c++# P14330 [JOI2021 预选赛 R2] 往返滑道 / Round Sugoroku ## 题目描述 JOI 高中的葵购买了一条新的滑道。该滑道由 $ N+2 $ 个格子横向排列组。这些格子从左至右依次编号为 $ 0 $ 到 $ N+1 $。初始时,格子 $ 0 $ 格子 $ N+1 $ 上写有字母 `x`,而格子 $ i $($ 1 \le i \le N $)上写有字符 $ S_i $。其中,$ S_i $ 为一个字母或符号 `#`。 葵使用这条滑道一个棋子进行游戏。初始时,棋子位于格子 $ A $($ 1 \le A \le N $),且朝向右方。注意,$ S_A $ 必须是一个字母。葵每隔 1 秒钟,将棋子向其当前朝向的方向移动 1 格。 滑道上设定有如下规则: - 当棋子落在写有 `x` 的格子上时,棋子的朝向会反转。 - 当棋子落在写有 `.` 的格子上时,不会发生任何变化。 - 当棋子落在写有 `#` 的格子上时,棋子的朝向会反转,且该格子上的字符会变为 `.`。此后,即使棋子再次落在该格子上,朝向也不会再反转。 此外,棋子反转方向或字符变更所耗费的时间可忽略不计。 当给定滑道与棋子的初始状态时,编写程序,计算所有写有 `#` 的格子均变为 `.` 所需的时间。 ## 输入格式 输入通过标准输入以如下格式给出: $ N $ $ A $ $ S $ 其中,$ S $ 是一个长度为 $ N $ 的字符串,其第 $ i $ 个字符($ 1 \le i \le N $)为 $ S_i $。 ## 输出格式 在标准输出中,输出一行:从开始到所有写有 `#` 的格子均变为 `.` 所需的秒数。 ## 输入输出样例 #1 ### 输入 #1 ``` 7 3 .#.#..# ``` ### 输出 #1 ``` 8 ``` ## 输入输出样例 #2 ### 输入 #2 ``` 4 1 .#.# ``` ### 输出 #2 ``` 7 ``` ## 输入输出样例 #3 ### 输入 #3 ``` 6 6 #####. ``` ### 输出 #3 ``` 35 ``` ## 说明/提示 ### 样例 1 解释 随着时间的推移,棋盘的状态将发生如下变化。将右向棋子所在的格子用 `>` 表示,将左向棋子所在的格子用 `<` 表示: ```  X.#>#..#X  X.#.<..#X  X.#<...#X  X.>....#X  X..>...#X  X...>..#X  X....>.#X  X.....>#X  X......<X ``` ### 样例 2 解释 ```  X>#.#X  X.<.#X  X<..#X  >...#X  X>..#X  X.>.#X  X..>#X  X...<X ``` ### 数据范围 - $ 2 \le N \le 200\,000 $。 - $ 1 \le A \le N $。 - $ S_i $ 为一个字母或符号 `#`($ 1 \le i \le N $)。 - $ S_A $ 为一个字母。 - 至少存在一个 $ i $($ 1 \le i \le N $),使得 $ S_i $ 为 `#`。 ### 子任务 1. (40 分)$ N \le 3\,000 $。 2. (60 分)无额外约束。 翻译由 Qwen3-235B 完
最新发布
10-29
<think>题目要求模拟一个棋子在滑道上的运动,直到所有的&#39;#&#39;都被转变为&#39;.&#39;。滑道由0到N+1共N+2个格子组,0N+1是固定的&#39;x&#39;,中间的1到N是字符串S(长度为N)。棋子初始在位置A(1<=A<=N),初始方向向右。每秒移动一格,根据格子上的字符有不同的行为: - 如果遇到&#39;x&#39;(只在0N+1),反转方向。 - 如果遇到&#39;.&#39;,什么都不发生。 - 如果遇到字母(题目说明初始时S_A是字母,但其他位置也可能是字母)?注意题目描述:格子i(1<=i<=N)上写有字符S_i,其中S_i为一个字母或符号&#39;#&#39;。但是,在运动过程中,只有&#39;#&#39;被触发后会变&#39;.&#39;,字母不会改变?但是题目中只提到当棋子落在写有字母的格子上时,不会发生任何变化吗?不,题目描述中只明确说了三类: - &#39;x&#39;: 反转方向(只在边界) - &#39;.&#39;: 无变化 - &#39;#&#39;: 反转方向,并且该格子变&#39;.&#39; 然而,题目没有提到字母的行为。重新阅读题目:格子0N+1是&#39;x&#39;,格子i(1<=i<=N)是S_i,S_i是字母或&#39;#&#39;。而规则中提到: - 落在&#39;x&#39;上:反转方向(边界) - 落在&#39;.&#39;上:无变化 - 落在&#39;#&#39;上:反转方向,且将该格子变&#39;.&#39; 那么字母呢?实际上,题目描述中并没有单独定义字母的规则。但是注意,字母&#39;.&#39;一样,都是非&#39;#&#39;&#39;x&#39;。然而,规则中只明确提到了&#39;.&#39;&#39;#&#39;,以及边界&#39;x&#39;。但题目中初始状态除了&#39;#&#39;&#39;.&#39;还有字母?再看输入格式:S是一个长度为N的字符串,第i个字符为S_i,且S_i是字母或&#39;#&#39;。并且保证初始时S_A是一个字母。 在规则中,没有提到字母的行为,那么我们可以认为字母的行为&#39;.&#39;一样吗?但是样例中并没有出现字母,输入样例中都是&#39;.&#39;&#39;#&#39;。再看题目描述:当棋子落在写有字母的格子上时,不会发生任何变化?实际上,题目描述规则时只说了三类:&#39;x&#39;, &#39;.&#39;, &#39;#&#39;。而对于字母并没有说明。那么我们必须从题目描述中寻找线索。 在题目描述中,规则部分只提到: - 棋子落在写有&#39;x&#39;的格子(边界)会反转。 - 落在写有&#39;.&#39;的格子,不会发生任何变化。 - 落在写有&#39;#&#39;的格子,反转方向,且将该格子变&#39;.&#39;。 没有提到字母,而字母在初始状态存在。但注意题目最后的数据范围指出:S_i是一个字母或&#39;#&#39;。而且保证初始位置A的字符S_A是一个字母。 但是,规则中没有定义字母的行为,这可能是题目的疏漏?或者我们应该理解:在滑道上,除了边界固定为&#39;x&#39;,中间格子只有两种状态:一种是可反转的障碍(用&#39;#&#39;表示,触发后变&#39;.&#39;),另一种就是普通格子(包括字母&#39;.&#39;)。那么对于普通格子,无论上面是字母还是&#39;.&#39;,行为应该是一样的:什么都不发生。 所以,我们可以将字母&#39;.&#39;视为同一类:即不会触发方向反转,也不会改变格子状态的格子(字母不会被改变,因为它不是&#39;#&#39;,所以不会触发变化)。因此,在模拟中,遇到字母遇到&#39;.&#39;是一样的处理:继续前进,不改变方向,也不改变格子状态。 因此,我们可以将格子分三种类型: 边界(0N+1):固定为&#39;x&#39;,反转方向。 中间格子(1~N): &#39;#&#39;:反转方向,并将该格子变为&#39;.&#39;(此后该格子就变了普通格子,即相当于字母或&#39;.&#39;)。 其他(字母或&#39;.&#39;):不改变方向,也不改变格子状态。 注意:初始状态可能有字母,但字母不会被改变,也不会触发反转。所以,我们只需要关注&#39;#&#39;边界&#39;x&#39;。 问题:需要模拟直到所有的&#39;#&#39;都变为&#39;.&#39;。 数据范围:N最大200000,但模拟可能时间很长(因为可能来回走)。例如样例3:6 6 "#####.",输出35。所以我们需要高效的方法。 由于N很大,直接模拟时间可能不够(因为时间可能很长,比如样例3有35秒,但如果是更大的N且有很多&#39;#&#39;,可能时间更长?实际上,题目要求的时间是秒数,而N最大20万,但模拟的步数可能很大,比如样例3中6个格子走了35步。所以我们需要寻找规律,避免一步步模拟。 观察:棋子只在遇到&#39;#&#39;或者边界&#39;x&#39;时才会反转方向。而且,一旦一个&#39;#&#39;被遇到就会变&#39;.&#39;,之后再经过就不会反转了。所以,每个&#39;#&#39;最多被访问一次(因为访问一次就变&#39;.&#39;了,以后就不再是&#39;#&#39;了)。 所以,整个过程其实就是棋子依次访问每个&#39;#&#39;的过程。直到所有的&#39;#&#39;都被访问过一次,游戏就结束了。 那么,总时间就是棋子从起点开始,依次访问所有&#39;#&#39;的位置(按访问顺序)所经过的时间。 但是,访问顺序并不是简单的从左到右或从右到左,因为方向会改变。 我们可以考虑:棋子的运动是在两个边界之间来回反弹,并且在遇到&#39;#&#39;时会反转方向(同时该&#39;#&#39;消失)。所以,我们可以将整个路线分解多个折返。 注意:边界反转&#39;#&#39;反转的效果是一样的,都是反转方向。不同之处在于:边界反转不会改变格子状态(因为边界始终是&#39;x&#39;),而&#39;#&#39;反转后该格子就变了普通格子(&#39;.&#39;)。 因此,我们可以这样:整个路径是由一系列从一端到另一端的线,每次遇到边界或者&#39;#&#39;就会反转。但是,由于&#39;#&#39;在消失后就不再反转,所以路径会随着时间而改变(因为障碍在减少)。 我们需要计算的是,从起点开始,直到所有的&#39;#&#39;都被访问一次,总共走了多少步。 一个关键点:每个&#39;#&#39;只会被访问一次(因为访问后就不再是障碍了),所以整个过程中访问的&#39;#&#39;的个数就是初始时字符串S中&#39;#&#39;的个数。我们记这个个数为M。 那么整个运动过程可以看作:棋子从起点出发,按照初始方向前进,直到遇到一个障碍(边界或&#39;#&#39;)就反转。我们关心的是,棋子按顺序访问每个&#39;#&#39;的位置,以及从起点到第一个&#39;#&#39;,然后到下一个&#39;#&#39;,直到最后一个&#39;#&#39;的时间。 但是,运动过程中,棋子会多次经过同一个格子(比如在来回走),但只有遇到&#39;#&#39;的时候才将其消除,并且反转方向。 我们需要计算的是总时间,即从开始到最后一个&#39;#&#39;被消除的时间。 思路: 将整个滑道分为左右两部分:以起点A为界,左边的部分(1~A-1右边的部分(A+1~N)。注意,起点A初始是字母(不会是&#39;#&#39;),所以起点不会触发反转。 棋子初始向右移动。那么它会先向右走,直到遇到一个障碍(可能是边界N+1,或者一个&#39;#&#39;)然后反转方向。然后向左走,直到遇到障碍(可能是边界0,或者一个&#39;#&#39;),再反转,如此反复。 但是,障碍中的&#39;#&#39;在第一次遇到时就会消失,所以后续再经过该位置时,它已经变普通格子,不会反转。 因此,我们可以记录下所有&#39;#&#39;的位置,然后模拟整个运动过程:记录当前方向,当前位置,以及已经消除的&#39;#&#39;的集合(或者计数)。但这样直接模拟,步数可能很大(比如最后一个样例,虽然只有6个格子,但是走了35步)。所以我们需要更高效的方法。 另一种思路:考虑每次反转之间的运动。 我们可以把运动分解为多个“”,每是从当前位置沿当前方向直到遇到一个障碍(边界或&#39;#&#39;)的路径。这样,每一的长度就是当前位置到下一个障碍的距离。然后,反转方向,继续下一。 但是,障碍中的&#39;#&#39;在消失后就不再是障碍,所以我们需要动态地知道当前方向上的下一个障碍是什么。 我们可以预先记录所有&#39;#&#39;的位置(按顺序存储),以及边界(0N+1)的位置。 然而,障碍包括边界未消除的&#39;#&#39;。由于边界是固定的,而&#39;#&#39;被消除后就从障碍中移除。 所以,我们可以维护两个有序集合(比如平衡树)或者两个链表:分别存储当前剩余的&#39;#&#39;在起点左侧在起点右侧的位置。但是起点也会移动?不对,我们关心的是当前方向上最近的障碍(包括边界未消除的&#39;#&#39;)。 注意:边界始终存在,所以障碍包括: 左边界0,右边界N+1,以及所有还未被消除的&#39;#&#39;的位置。 因此,在任意时刻,向左运动时,下一个障碍就是当前位置左侧最近的一个障碍(可能是0,也可能是某个&#39;#&#39;)。向右运动时,下一个障碍就是当前位置右侧最近的一个障碍(可能是N+1,也可能是某个&#39;#&#39;)。 那么,我们可以用两个有序数据结构(例如两个set)分别存储当前位置左边右边的未消除的&#39;#&#39;的位置。初始时,我们可以将所有的&#39;#&#39;的位置加入这两个数据结构?或者我们可以用一个数据结构(如平衡树)来维护所有未消除的&#39;#&#39;的位置,然后每次查询当前方向上的下一个障碍(即下一个&#39;#&#39;,然后比较边界的距离,取最近的那个)。 但是,障碍包括边界未消除的&#39;#&#39;,所以下一个障碍的位置应该是: 当前方向上的第一个未消除的&#39;#&#39;的位置,以及边界(0或N+1)中离当前位置更近的那个?不对,因为边界是固定的,所以下一个障碍一定是当前方向上最近的未消除的&#39;#&#39;或者边界(如果中间没有未消除的&#39;#&#39;,那么边界就是下一个障碍)。 因此,我们可以这样维护: 用两个set:leftSet(存储当前位置左侧的未消除的&#39;#&#39;的位置)rightSet(存储右侧的),或者一个set维护所有未消除的&#39;#&#39;的位置,然后利用lower_bound等操作来查询下一个。 具体: 设当前位置为pos,方向为dir(1表示右,-1表示左)。 向右运动:下一个障碍是右边第一个未消除的&#39;#&#39;的位置,以及右边界N+1。取min(右边第一个&#39;#&#39;的位置, N+1)?不对,因为右边第一个&#39;#&#39;可能比N+1更近。实际上,下一个障碍就是min(右边第一个&#39;#&#39;的位置, N+1)吗?不对,如果右边没有&#39;#&#39;,那么障碍就是N+1;如果有,则取最近的(即最小的那个位置)。实际上,在向右运动时,下一个障碍应该是当前位置右侧最近的障碍,即min{右边所有未消除的&#39;#&#39;的位置,N+1}。但我们只需要最小的那个位置(因为向右走,所以第一个遇到的是最小的位置)。 同理,向左运动:下一个障碍是max(左边第一个&#39;#&#39;的位置, 0)? 不对,向左运动,位置是递减的,所以下一个障碍应该是当前位置左侧的最大位置(即离当前位置最近的一个障碍在左侧的最大值?)。具体:向左运动,我们希望找到当前位置左侧最近的一个障碍(也就是位置最大的一个,因为从当前位置向左,位置值减小,所以左侧的位置都比当前位置小,那么最大的位置就是离当前位置最近的)。 所以,我们维护一个有序集合(如set)来存储所有未消除的&#39;#&#39;的位置。然后,对于当前方向位置: 如果方向向右:我们查找set中大于当前位置的最小元素(即lower_bound),如果存在,则候选障碍为min(该元素, N+1);如果不存在,则候选障碍就是N+1。 但是,这里我们只需要考虑最近的一个,所以候选障碍就是:如果set中存在大于pos的元素,则取最小的那个(记为next_right),然后障碍位置为min(next_right, N+1)?不对,因为N+1是边界,即使中间有&#39;#&#39;,我们也只能先遇到&#39;#&#39;(如果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的未消除&#39;#&#39;,则取那个位置;否则,取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是&#39;#&#39;(一定在1~N之间):那么除了反转方向,我们还要将这个&#39;#&#39;从set中移除(表示消除),并且消除计数加1。 然后,更新当前位置为next_obstacle,方向反转,重复上述过程。 直到set为空(即所有&#39;#&#39;都被消除),此时结束,输出总时间。 注意:当消除最后一个&#39;#&#39;后,我们不需要再继续走到边界?题目要求是直到所有&#39;#&#39;都变为&#39;.&#39;,所以最后一次消除后就可以结束了?但是,根据规则,当棋子落在&#39;#&#39;上时,会反转方向并且该格子变为&#39;.&#39;。也就是说,在消除最后一个&#39;#&#39;的那个时刻,我们还要反转方向?反转方向后,游戏并没有结束,因为题目要求所有&#39;#&#39;都变为&#39;.&#39;,而我们在反转方向的那个时刻就已经消除了最后一个&#39;#&#39;,所以此时已经满足条件。因此,最后一步(即消除最后一个&#39;#&#39;的那一步)执行完,游戏就结束了。 所以,我们模拟的过程就是:不断进行下一,直到所有&#39;#&#39;被消除。 但是,我们需要注意:在走到边界的时候,我们并不消除任何&#39;#&#39;,所以即使此时set为空,我们还需要继续?不对,因为边界反转后,我们还要继续运动,直到再次遇到障碍(但此时已经没有&#39;#&#39;了,所以下一个障碍就是边界?然后反转,再继续?这样会无限循环?)。但是,题目要求的是所有&#39;#&#39;都变为&#39;.&#39;,所以当最后一个&#39;#&#39;被消除之后,游戏就应该结束了。也就是说,在最后一次消除&#39;#&#39;后,我们就不需要再继续模拟了。因此,我们在消除最后一个&#39;#&#39;后,就可以结束模拟,将当前累计时间加上从当前位置到消除那个&#39;#&#39;的时间(即最后一步)就是总时间。 所以,在模拟循环中,当我们移除一个&#39;#&#39;后,检查set是否为空。如果为空,则退出循环,此时总时间就是答案。 但是,注意:消除一个&#39;#&#39;的操作是在我们到达该位置时发生的,所以我们在移除该&#39;#&#39;后,如果set为空,则结束。 因此,算法框架: total_time = 0 pos = A dir = 1 (向右) 用一个set(或者两个set,但为了查询效率,用一个set)存储所有&#39;#&#39;的位置(初始时,遍历字符串S,记录所有S[i]==&#39;#&#39;的位置i+1,因为字符串S的索引0对应格子1,注意格子编号1~N)。 注意:格子编号0N+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: // 遇到一个&#39;#&#39; // 注意:这里next_obstacle一定是一个&#39;#&#39; dir = -1 // 反转方向 // 消除这个&#39;#&#39;:从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: // 遇到一个&#39;#&#39; dir = 1 // 反转方向 // 消除这个&#39;#&#39;:从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失效,但我们在移除之前已经保存了值,所以移除后不再使用该迭代器。 然而,在向左移动的分支中,我们使用了两个迭代器:itit_prev。我们移除的是it_prev,而it并没有被移除,所以移除it_prev不会影响it(因为set的迭代器除了被移除的,其他仍然有效?标准规定,set的erase只使被移除元素的迭代器失效,其他迭代器不受影响)。 但是,实际上我们移除it_prev后,it可能失效吗?不会,因为it_previt是两个不同的迭代器,而且it_prev是it的前一个,移除it_prev并不会影响it(只要it没有被移除,它就仍然有效)。 但是,在移除后,我们检查set为空,然后break,所以没有问题。 但是,有一个问题:在向右移动时,我们使用obstacles.lower_bound(pos+1)得到it,然后如果遇到的是&#39;#&#39;,我们就移除it。这样移除是安全的。 在向左移动时,我们得到it_prev,然后移除it_prev,也是安全的。 但是,我们需要考虑:当我们删除一个元素后,下一次循环中,我们再次查询时,这个元素已经被移除。 因此,这个模拟过程应该能够正确执行。 但是,我们需要验证样例。 样例1:N=7, A=3, S=".#.#..#"(注意字符串长度为7,索引0~6) 格子1~7:S[0]=&#39;.&#39; ->格子1, S[1]=&#39;#&#39; ->格子2, S[2]=&#39;.&#39; ->格子3, S[3]=&#39;#&#39; ->格子4, S[4]=&#39;.&#39; ->格子5, S[5]=&#39;.&#39; ->格子6, S[6]=&#39;#&#39; ->格子7 初始位置A=3(格子3,位置值为3),初始方向向右。 set初始:包含位置2,4,7(因为格子2,4,7是&#39;#&#39;) 第一次:向右移动,当前位置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: &#39;.&#39;,格子2: &#39;#&#39;,格子3: &#39;.&#39;,格子4: &#39;#&#39; 初始位置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个是&#39;#&#39;,最后一个&#39;.&#39;。所以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。 因此,这个模拟算法是正确的。 但是,时间复杂度:我们每次循环消除一个&#39;#&#39;,而&#39;#&#39;的个数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]==&#39;#&#39;,则将位置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] == &#39;#&#39;) { 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] == &#39;#&#39;) { 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可能包含字母,但我们的处理只关心&#39;#&#39;,所以字母被忽略(不会加入obstacles),因此字母等同于&#39;.&#39;。 因此,这个算法是符合题意的。 但是,我们还需要考虑一个边界情况:初始位置A的字符是字母(题目保证),所以不会在初始位置触发反转。 另外,如果初始位置A恰好有一个&#39;#&#39;?但题目保证S_A是一个字母,所以初始位置不会是&#39;#&#39;。 因此,代码应该正确。 我们提交到洛谷测试。 由于题目要求,我们使用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] == &#39;#&#39;) { 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`(正确)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值