写在2020前面

2019年已经过去了,这一年,发生了很多重要时刻,第一个孩子出生视为第一重要的时刻。回收过去一年不管是在生活上还是做得不够多。学习上,2019年还是很懒散。先说说接下来2020想做的事
1) Spring文档翻译(通读,带源码笔注),带源码解读
2)CMU编译原理(带笔记,lab),龙书
3)Csapp + 书 (带笔记+lab)
4)JDK源码(带源码笔注)
希望上面立的flag能实现,不敢写太多并且将微博发出来。国外高校基础课才是财富

<think>嗯,用户问的是NOIP2020的移动小球问题。首先我需要回忆一下这个题目的具体内容。NOIP是中国信息学奥林匹克联赛,题目通常涉及算法和数据结构。移动小球的问题可能涉及到链表或者双指针之类的数据结构,因为题目通常需要高效地处理插入和删除操作。 首先,我应该先确认题目的具体描述。题目大意可能是这样的:有多个小球按顺序排列,然后进行一系列操作,每次移动某个小球到另一个小球的左边或右边,最后输出最终的排列。例如,初始排列是1,2,3,...,n,然后每次操作比如把a移到b的左边,或者右边,最后输出结果。 这样的问题,如果用数组处理,每次移动操作的时间复杂度会很高,因为需要移动大量元素。所以应该考虑更高效的数据结构,比如双向链表。链表可以在O(1)的时间内完成插入和删除,但需要能够快速找到对应节点的位置。 接下来需要思考如何实现。每个节点需要记录前驱和后继,这样移动的时候只需要调整相关节点的指针。比如,如果要把a移到b的左边,那么需要将a从当前位置删除,然后插入到b的前面。这涉及到更新a的前驱和后继,以及b原来的前驱的后继,还有b的前驱指向a等操作。 但如何快速找到每个小球对应的节点呢?可能需要一个数组或者哈希表来保存每个小球的节点信息,比如每个位置存储它的前驱和后继。例如,用left和right两个数组,left[i]表示i号球左边的球,right[i]表示右边的球。这样,当要移动a到b的左边时,可以: 1. 先将a从原位置移除,即把a原来的前驱和后继连接起来。 2. 然后将a插入到b的左边,即设置a的前驱为b原来的前驱,a的后继为b,然后更新b的前驱为a,以及原来的前驱的后继为a。 这样每一步操作都是O(1)的时间,非常高效。初始的时候,每个球i的左边是i-1,右边是i+1,除了两端的球。例如,n=5的话,初始left数组是[0,1,2,3,4],right数组是[2,3,4,5,6],可能索引需要调整,比如球从1到n,数组的索引从1到n。 然后,处理完所有操作后,需要遍历链表,输出最终的顺序。可以从最左边的节点开始,一直找right直到结束。或者找到第一个节点(比如left[i]=0的那个),然后依次输出right。 那么具体的实现步骤应该是怎样的呢?首先初始化left和right数组。然后处理每个操作,分为移动到左边或右边两种情况。需要注意的是,在移动的时候,要正确处理节点的前后关系,否则可能会导致错误。 举个例子,比如初始状态是1-2-3-4-5。现在要把2移动到4的左边。那么原来的链中,2的前驱是1,后继是3。移动后,4的前驱是2,而原来的4的前驱是3,所以需要先将2从1和3之间断开,然后插入到3和4之间。也就是: 当移动a到b左边时: 1. 将a从当前位置删除:right[left[a]] = right[a],left[right[a]] = left[a] 2. 将a插入到b的左边:left[a] = left[b],right[a] = b 3. 更新原left[b]的right为a,以及left[b] = a 这样就能完成插入操作。同样,如果是移动到右边,那么插入的位置是b的右边,操作类似。 需要注意的是,当移动的a和b相邻时,比如原本a在b的左边,此时移动a到b左边可能不需要改变,或者需要正确处理指针,否则可能会导致循环引用或者其他错误。所以在处理过程中必须确保所有相关节点的前驱和后继都被正确更新。 实现的时候,可以使用数组来存储每个节点的left和right。例如,left和right数组的大小为n+1(因为球的编号是1到n),初始时,对于每个i,left[i] = i-1,right[i] = i+1。但是左右边界的处理可能需要特殊处理,比如left[1] = 0,right[n] = 0或者n+1,视具体实现而定。 在处理完所有操作后,如何输出结果呢?可以从第一个节点开始,即找到left[i] = 0的节点,也就是第一个节点,然后依次访问right直到遇到0。或者,因为初始时第一个节点是1,但是经过多次操作后,可能第一个节点已经变了。所以可能需要找到哪个节点的left是0,然后开始遍历。 例如,初始第一个节点是1,它的left是0。在操作过程中,如果有节点被移动到整个序列的最前面,比如把某个节点移到第一个节点的左边,此时第一个节点的left会被更新,而新节点的left变为0,这时候需要以这个新节点作为起始点。 因此,输出时需要找到当前链表的头节点,也就是left为0的那个节点,然后根据right数组依次输出每个节点,直到right为0为止。 现在需要考虑如何测试这样的算法是否正确。例如,假设n=5,初始顺序1-2-3-4-5。然后进行操作,比如将2移到4左边,那么顺序变为1-3-4-2-5?或者应该是1-3-2-4-5?或者需要再仔细想。 比如,原链中,2的左边是1,右边是3。当把2移到4左边的话,4原来的左边是3。所以插入后的顺序应该是将2放在3和4之间。所以顺序变成1-3-2-4-5。那正确的操作应该是这样。这时候,链表的结构是:1的right是3,3的left是1,right是2;2的left是3,right是4;4的left是2,right是5。 所以,这个例子中,输出应该是1 3 2 4 5。这时候,通过遍历链表是否正确? 是的。那这说明算法的正确性。 那在代码实现时,如何处理多个操作后的结果?例如,可能有很多次移动,每次都需要正确更新left和right数组。 现在,关于具体的代码结构,可能需要用数组来保存left和right。例如,在C++中: const int MAXN = 1e5 + 10; // 假设n最大是1e5 int left[MAXN], right[MAXN]; 初始化时,对于i从1到n: left[i] = i-1; right[i] = i+1; 然后,left[1] = 0,right[n] = 0。或者根据题目描述中的情况,比如是否是环形排列?例如,是否第一个元素的左边是最后一个?但原题应该是线性排列,不是环形的。所以头节点的left是0,尾节点的right是0。 那在移动操作中,比如将a移动到b的左边: 第一步,将a从原位置移除: right[left[a]] = right[a]; left[right[a]] = left[a]; 第二步,将a插入到b的左边: left[a] = left[b]; right[a] = b; 然后,原来的left[b]的right应该指向a: right[left[b]] = a; 最后,left[b] = a; 例如,假设b原来的左边是c,那么现在c的右边是a,a的左边是c,a的右边是b,b的左边是a。 同样的,如果操作是将a移动到b的右边,那么插入的位置是b的右边。这时候: 将a从原位置移除,然后插入到b的右边: left[a] = b; right[a] = right[b]; 然后,原来的right[b]的左边指向a: left[right[b]] = a; right[b] = a; 这样,就完成了插入到右边的操作。 那么,代码中的每个移动操作可以分为两种情况,左或右,然后分别处理。 接下来,如何处理输入?假设输入的第一行是n和m,n是小球的数目,m是操作次数。然后接下来的m行,每行给出两个数a和b,或者可能还有方向参数?比如,每个操作可能是将a移动到b的左边或右边,所以需要参数来指示方向。例如,每个操作是三个参数:a, b, dir(比如0表示左边,1表示右边)。 比如,输入可能是这样的: 5 3 1 3 0 2 4 1 ... 不过,具体要看题目的输入格式。假设题目中的每个操作是给出两个数a和b,表示将a移动到b的左边或右边。例如,原题可能的输入格式是每个操作有两个数x和y,表示将x移到y的左边。或者可能有不同的参数。例如,NOIP2020的题目具体描述可能需要查证。但根据用户的问题,可能题目中的操作是每次移动某个球到另一个球的左边或右边,所以每个操作应该包含这些信息。 不过,假设用户的问题中的移动小球题目是类似于这样的:每个操作给出x和y,将x移到y的左边;如果x已经在y左边,则不动。或者不管原来的位置,都执行移动。所以需要处理所有情况。 现在,不管原来的位置如何,都要将x移动到y的左边。这时候,每次操作都需要将x从原位置删除,然后插入到y的左边。例如,即使x本来就在y左边,也需要先删除再插入。这样,如果x和y相邻,比如x原来在y左边,这时候删除x再插入到y左边,可能不会有变化,但代码需要处理这种情况。 或者,如果x和y是同一个球,是否需要处理?可能题目保证不会出现这种情况,或者需要忽略这样的操作。 现在,假设所有操作都是有效的,即x和y不同,且存在。那么在代码中不需要处理这些特殊情况。 接下来,如何将所有的操作处理完后输出结果? 需要从头节点开始遍历。头节点是哪个?即left为0的节点。例如,初始时头节点是1。但在操作过程中,可能有其他节点被移动到最前面。例如,假设将3移动到1的左边,那么3的左边变为0,头节点变成3,而原来的头节点1的左边变成3的右边。此时,遍历应该从3开始。 所以,遍历的方法是: 初始化当前节点为头节点,即找到i满足left[i] == 0。然后依次访问right[current],直到current为0。 例如,在代码中可以这样: int current = 0; for (int i=1; i<=n; ++i) { if (left[i] == 0) { current = i; break; } } 然后,依次输出current,然后current = right[current],直到current为0。 或者,更高效的方式是,因为可能有多个节点的left为0?这应该是不可能的,因为链表的结构是每个节点只有一个前驱和一个后继,所以整个结构是线性的,只有一个头节点。 因此,遍历时只需要找到第一个left为0的节点,然后顺序访问right即可。 但是,如果链表中间某个节点的left被错误地设置为0,那么遍历就会出错。所以在代码中必须确保链表结构的正确性。 现在,回到问题,用户需要介绍NOIP2020的移动小球问题。所以,我应该给出题目的大意、解法思路,以及可能的代码实现,或者关键部分的代码。 可能的步骤: 1. 题目描述:初始有n个小球排成一列,编号1到n。进行m次操作,每次将小球x移动到小球y的左边或右边。最后输出最终的排列顺序。 2. 解法分析:使用双向链表模拟,每个节点记录左右邻居。用数组存储前驱和后继,每次操作O(1)时间。 3. 关键点:如何高效处理移动操作,以及如何遍历链表输出结果。 4. 代码框架:例如,使用left和right数组,初始化,处理每个操作,最后遍历输出。 举个例子,假设n=5,m=2次操作: 操作1:将2移动到4的左边。 操作2:将5移动到1的右边。 初始链表:1-2-3-4-5 操作1后:1-3-4-2-5 → 对吗?或者是否应为1-3-2-4-5? 或者,我需要重新仔细分析。比如,移动2到4的左边: 原链中,4的左边是3。所以插入后的顺序是1-3-2-4-5。是的。那么操作后顺序是1,3,2,4,5。 操作2:将5移到1的右边。此时,1的右边原本是3。插入后,5被插入到1的右边,原来的1的右边变成5,5的左边是1,右边是原来的3。而原来的3的左边变成5。所以顺序变为1-5-3-2-4。然后,正确的输出是1 5 3 2 4? 或者,可能我在这里的思考有误,需要仔细检查。 或者,当把x移动到y的左边时,不管原来的位置如何,都执行删除和插入操作。例如,在操作2中,把5移动到1的右边,相当于移动到1的右边,也就是在1和它原来的右边节点之间插入5。或者可能操作是移动到右边,即插入到y的右边。例如,移动到y的右边,应该将x放在y的后面,即y的右边变成x,x的左边是y,x的右边是y原来的右边。然后,y原来的右边的左边变成x。 比如,如果要将5移到1的右边: 原链中,1的右边是3(操作1后的顺序是1-3-2-4-5)。所以,执行将5移动到1的右边: 首先,删除5:原来的5的左边是4,右边是0。所以删除操作后,4的右边变成0,而原来的right[5]是0,所以left[0]会被设置为4?这可能有问题,因为数组索引0是否被允许。或者,在代码中,假设left和right数组的索引是1到n,而0表示空。 所以,将5从原位置删除:它的左边是4,右边是0。所以,删除时: right[left[5]] = right[5]; → right[4] = 0; left[right[5]] = left[5]; → 这里的right[5]是0,left[0]会被设置为4?但是0可能是一个无效的索引,所以这行代码可能可以忽略,因为如果right[5]是0的话,说明5是最后一个节点,此时不需要处理后面的节点(因为没有后面的节点)。 所以,在删除的时候,只需要处理前驱和后继,当它们存在时。例如,在删除x的时候: 如果left[x] != 0 → right[left[x]] = right[x] 如果 right[x] !=0 → left[right[x]] = left[x] 这样可以避免处理0的情况。 然后,插入到y的右边: left[x] = y; right[x] = right[y]; 如果 right[y] !=0 → left[right[y]] = x; right[y] = x; 这样,当y的右边原来存在节点的话,就更新它们的左边指向x,否则(即y是最后一个节点),不需要处理。 所以,在操作2中,将5移动到1的右边: 此时,y是1,它的右边是3。将x=5插入到y的右边,即: left[5] = 1 right[5] = right[1] → 3 然后,right[y] =5 → right[1] =5 接着,原来的right[y]是3,所以left[3] =5. 所以,现在: 1的右边是5,5的右边是3,3的左边是5,5的左边是1。原来的顺序是1-3-2-4-5,现在变成1-5-3-2-4。而5原来的位置在最后,现在被移动到了1的右边,所以顺序是1 5 3 2 4。最后的链表中,4的右边是0,因为原来的5被移走了,所以4的右边是0。此时,5的右边是3,3的右边是2,依此类推。 所以最终的顺序是1 →5→3→2→4,对吗? 因此,经过两次操作后的输出是1 5 3 2 4。这说明代码的正确性。 现在,总结解法步骤: 1. 初始化left和right数组,每个节点i的left[i] = i-1,right[i] = i+1。left[1] =0,right[n]=0. 2. 处理每个操作: a. 如果要移动x到y的左边: i. 将x从当前位置删除:如果left[x]存在(不为0),则right[left[x]] = right[x]; 同理处理right[x]的left. ii. 将x插入到y的左边:设置x的left为y的left,x的right为y。然后更新y的left的right为x,最后将y的left设为x. b. 类似处理移动到右边的操作。 3. 最后遍历链表,从head开始(left为0的节点),依次访问right直到0。 现在,关于代码实现,可以给出伪代码或者C++的示例: 例如: #include <iostream> using namespace std; const int N = 1e5 + 10; int left_[N], right_[N]; void link(int x, int y) { right_[x] = y; left_[y] = x; } // 将x移动到y的左边 void move_left(int x, int y) { // 先删除x if (left_[x] != 0) right_[left_[x]] = right_[x]; if (right_[x] != 0) left_[right_[x]] = left_[x]; // 插入到y的左边 int l = left_[y]; link(l, x); link(x, y); } // 将x移动到y的右边 void move_right(int x, int y) { // 先删除x if (left_[x] != 0) right_[left_[x]] = right_[x]; if (right_[x] != 0) left_[right_[x]] = left_[x]; // 插入到y的右边 int r = right_[y]; link(y, x); link(x, r); } int main() { int n, m; cin >> n >> m; // 初始化 for (int i = 1; i <= n; ++i) { left_[i] = i - 1; right_[i] = i + 1; } right_[n] = 0; // 末尾的右边是0 while (m--) { int x, y, op; cin >> x >> y >> op; if (op == 0) { // 移动到y的左边 move_left(x, y); } else { // 移动到右边 move_right(x, y); } } // 找到头节点 int head = 0; for (int i = 1; i <= n; ++i) { if (left_[i] == 0) { head = i; break; } } // 遍历输出 int curr = head; while (curr != 0) { cout << curr << " "; curr = right_[curr]; } cout << endl; return 0; } 注意,这里假设每个操作包含三个参数:x,y,op(op为0表示左边,1表示右边)。需要根据题目的实际输入格式进行调整。 此外,删除x的操作必须进行两次判断,即如果x的左边或右边存在节点,才进行更新。例如,当x是头节点时,左边是0,所以不需要处理左边的节点的右边指针。同样,当x是尾节点时,右边是0,不需要处理右边的节点的左边指针。 这样,整个算法的时间复杂度是O(n + m),可以处理较大的数据规模,例如n和m在1e5级别,这在NOIP中是可行的。 总结一下,这个问题的关键在于使用双向链表来高效处理移动操作,而数组实现的链表可以满足时间复杂度的要求。此外,正确维护前驱和后继的指针是解决问题的关键。</think>### NOIP2020 移动小球问题详解 #### 题目大意 初始有 $n$ 个小球按编号 $1,2,...,n$ 顺序排列。进行 $m$ 次操作,每次将小球 $x$ 移动到小球 $y$ 的左边或右边。求最终排列顺序。 --- #### 算法思路 1. **数据结构选择** 使用 **双向链表** 维护小球顺序,通过数组模拟链表结构: - `left[i]` 表示小球 $i$ 的左侧小球编号。 - `right[i]` 表示小球 $i$ 的右侧小球编号。 2. **操作处理** - **移动小球到左侧**:将 $x$ 从原位置删除,插入到 $y$ 的左侧。 - **移动小球到右侧**:将 $x$ 从原位置删除,插入到 $y$ 的右侧。 3. **复杂度分析** 每次操作仅需 $O(1)$ 时间,总时间复杂度为 $O(n + m)$,适用于大规模数据。 --- #### 关键步骤 1. **初始化链表** 初始时,小球 $i$ 的左右邻居分别为 $i-1$ 和 $i+1$: ```plaintext left[i] = i - 1 right[i] = i + 1 ``` 边界处理:`left[1] = 0`,`right[n] = 0`(0 表示空节点)。 2. **删除操作** 将小球 $x$ 从链表中移除: ```plaintext right[left[x]] ← right[x] left[right[x]] ← left[x] ``` 3. **插入操作** - **插入到左侧**:将 $x$ 插入到 $y$ 的左侧: ```plaintext left[x] ← left[y] right[x] ← y right[left[y]] ← x left[y] ← x ``` - **插入到右侧**:将 $x$ 插入到 $y$ 的右侧: ```plaintext left[x] ← y right[x] ← right[y] left[right[y]] ← x right[y] ← x ``` 4. **遍历输出** 找到头节点(`left[i] = 0` 的节点),依次输出每个节点的右侧节点。 --- #### 代码实现(C++ 框架) ```cpp #include <iostream> using namespace std; const int N = 1e5 + 10; int left_[N], right_[N]; // 将 x 插入到 y 的左侧 void move_left(int x, int y) { // 删除 x if (left_[x]) right_[left_[x]] = right_[x]; if (right_[x]) left_[right_[x]] = left_[x]; // 插入到 y 左侧 left_[x] = left_[y]; right_[x] = y; if (left_[y]) right_[left_[y]] = x; left_[y] = x; } // 将 x 插入到 y 的右侧 void move_right(int x, int y) { // 删除 x if (left_[x]) right_[left_[x]] = right_[x]; if (right_[x]) left_[right_[x]] = left_[x]; // 插入到 y 右侧 right_[x] = right_[y]; left_[x] = y; if (right_[y]) left_[right_[y]] = x; right_[y] = x; } int main() { int n, m; cin >> n >> m; // 初始化链表 for (int i = 1; i <= n; i++) { left_[i] = i - 1; right_[i] = i + 1; } right_[n] = 0; // 末尾节点右侧为空 // 处理操作 while (m--) { int x, y, op; cin >> x >> y >> op; if (op == 0) move_left(x, y); else move_right(x, y); } // 找到头节点 int head = 0; for (int i = 1; i <= n; i++) { if (left_[i] == 0) { head = i; break; } } // 输出结果 int curr = head; while (curr != 0) { cout << curr << " "; curr = right_[curr]; } return 0; } ``` --- #### 示例分析 - **输入**: ``` 5 2 2 4 0 // 将2移到4左侧 5 1 1 // 将5移到1右侧 ``` - **处理过程**: 1. 初始链表:$1 \leftrightarrow 2 \leftrightarrow 3 \leftrightarrow 4 \leftrightarrow 5$ 2. 移动2到4左侧后:$1 \leftrightarrow 3 \leftrightarrow 2 \leftrightarrow 4 \leftrightarrow 5$ 3. 移动5到1右侧后:$1 \leftrightarrow 5 \leftrightarrow 3 \leftrightarrow 2 \leftrightarrow 4$ - **输出**:`1 5 3 2 4` --- #### 总结 通过双向链表高效维护动态插入和删除操作,确保每次操作时间复杂度为 $O(1)$,适用于 NOIP 竞赛中大规模数据场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值