单链表
邻接表:n个链表,用于存储图和树
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int head, e[N], ne[N], idx;void init () { head = -1 ; idx = 0 ; } void inset (int a) { e[idx] = a, ne[idx] = head, head = idx ++; } void remove () { head = ne[head]; }
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <iostream> using namespace std;const int N = 100010 ;int head, e[N], ne[N], idx;void init () { head = -1 ; idx = 0 ; } void insert (int x) { e[idx] = x, ne[idx] = head, head = idx ++ ; } void insert (int k, int x) { e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ; } void remove (int k) { ne[k] = ne[ne[k]]; } int main () { int n; scanf ("%d" , &n); init (); while (n --){ int k, x; char op; cin >> op; if (op == 'H' ){ cin >> x; insert (x); } else if (op == 'D' ){ cin >> k; if (k) remove (k - 1 ); else head = ne[head]; } else { cin >> k >> x; insert (k - 1 , x); } } for (int i = head; i != -1 ; i = ne[i]) cout << e[i] << ' ' ; puts ("" ); return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def main (): N = 100010 head = - 1 e = [0 ] * N ne = [0 ] * N idx = 0 def insert_head (x ): nonlocal idx, head e[idx] = x ne[idx] = head head = idx idx += 1 def insert (k, x ): nonlocal idx e[idx] = x ne[idx] = ne[k] ne[k] = idx idx += 1 def remove (k ): ne[k] = ne[ne[k]] m = int (input ()) while m: data = input ().split() if data[0 ] == "H" : x = int (data[1 ]) insert_head(x) elif data[0 ] == "D" : k = int (data[1 ]) if k: remove(k - 1 ) else : head = ne[head] else : k, x = int (data[1 ]), int (data[2 ]) insert(k - 1 , x) m -= 1 i = head res = [] while i != -1 : res.append(e[i]) i = ne[i] print (" " .join(map (str , res))) if __name__ == "__main__" : main()
双链表
用于优化某些问题
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> using namespace std;const int N = 100010 int m;int e[N], l[N], r[N], idx;void init () { r[0 ] = 1 , l[1 ] = 0 ; idx = 2 ; } void insert (int a, int x) { e[idx] = x; l[idx] = a, r[idx] = r[a]; l[r[a]] = idx, r[a] = idx ++ ; } void remove (int a) { l[r[a]] = l[a]; r[l[a]] = r[a]; }
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;int n;int e[N], l[N], r[N], idx;void init () { r[0 ] = 1 , l[1 ] = 0 ; idx = 2 ; } void insert (int k, int x) { e[idx] = x; l[idx] = k, r[idx] = r[k]; l[r[k]] = idx, r[k] = idx ++; } void remove (int k) { r[l[k]] = r[k]; l[r[k]] = l[k]; } int main () { scanf ("%d" , &n); init (); while (n -- ){ int k, x; string op; cin >> op; if (op == "L" ){ cin >> x; insert (0 , x); } else if (op == "R" ){ cin >> x; insert (l[1 ], x); } else if (op == "D" ){ cin >> k; remove (k + 1 ); } else if (op == "IL" ){ cin >> k >> x; insert (l[k + 1 ], x); } else { cin >> k >> x; insert (k + 1 , x); } } for (int i = r[0 ]; i != 1 ; i = r[i]) cout << e[i] << " " ; cout << endl; return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 def main (): N = 100010 e = [0 ] * N l = [0 ] * N r = [0 ] * N r[0 ], l[1 ] = 1 , 0 idx = 2 def insert (k, x ): nonlocal idx e[idx] = x l[idx], r[idx] = k, r[k] l[r[k]] = idx r[k] = idx idx += 1 def remove (k ): l[r[k]] = l[k] r[l[k]] = r[k] m = int (input ()) while m: data = input ().split() if data[0 ] == "L" : x = int (data[1 ]) insert(0 , x) elif data[0 ] == "R" : x = int (data[1 ]) insert(l[1 ], x) elif data[0 ] == "D" : k = int (data[1 ]) remove(k + 1 ) elif data[0 ] == "IL" : k, x = int (data[1 ]), int (data[2 ]) insert(l[k + 1 ], x) else : k, x = int (data[1 ]), int (data[2 ]) insert(k + 1 , x) m -= 1 i = r[0 ] res = [] while i != 1 : res.append(e[i]) i = r[i] print (" " .join(map (str , res))) if __name__ == "__main__" : main()
栈
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int stk[N], tt = 0 ;stk[ ++ tt] = x; tt -- ; stk[tt]; if (tt > 0 ){}
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;int m;int stk[N], tt;int main () { cin >> m; while (m --){ int x; string op; cin >> op; if (op == "push" ){ cin >> x; stk[++ tt] =x; } else if (op == "query" ) cout << stk[tt] << endl; else if (op == "pop" ) tt --; else if (op == "empty" ) cout << (tt ? "NO" : "YES" ) << endl; } return 0 ; }
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def main (): n = int (input ()) stk = [] while n: q = input ().split() if q[0 ] == "push" : stk.append(int (q[1 ])) elif q[0 ] == "pop" : stk.pop() elif q[0 ] == "empty" : print ("YES" if len (stk) == 0 else "NO" ) elif q[0 ] == "query" : print (stk[-1 ]) n -= 1 if __name__ == "__main__" : main()
一. 问题分析
1. 信息提取
+
和 -
等价, *
和 /
等价
所以只需分析 +
和 *
2. 总结特点
二. 问题建模
1. 中序遍历表达式树的计算过程
任意一个表达式树
叶节点是数字, 内部节点是运算符
计算过程
遍历节点 1 2 3 后, 则 4 的左子树遍历完, 则计算 4 的左子树的结果, 新节点作为 4 的左孩子节点
继续遍历节点 5 6 7, 则 8 的左子树遍历完, 则计算 8 的左子树的结果, 新节点作为 8 的左孩子节点
同理, 遍历节点 12 的时候计算其左子树的结果, 新节点作为 12 的左孩子
当整棵树遍历完后, 再次逆序计算各运算符的结果, 最后只剩一个节点就是表达式的最终结果
注意计算 8 的左子树时先计算运算符 6, 然后结果作为运算符 4 的右孩子, 然后计算运算符 4, 其结果作为运算符 8 的左孩子
2. 计算过程分析
如何判断某棵子树被遍历完 ?
中序遍历往上走时, 子树遍历完
例如过程 1 中遍历节点 4 时, 说明 1 2 3 的子树遍历完
中序遍历往下走时, 子树未遍历完
例如过程 2 中遍历节点 6 时, 相对于 4 是往下走的, 此时 8 的左子树未遍历完
如何判断往上走还是往下走 ?
注意到运算符优先级大的在下面, 运算符优先级小的在上面
所以当目前运算符的优先级比上一运算符优先级小时, 说明是往上走
当目前运算符的优先级比上一运算符优先级大是, 说明是往下走
什么时候进行计算 ?
三. 问题解法
1. 数据结构
由于是模拟中序遍历树的过程, 所以要用栈数据结构
由于是有运算符和数字两个对象, 所以要用两个栈来存储
2. 算法
遇到各节点后的处理
数字
数字并不会产生计算过程, 所以只需提取数字, 将数字压栈
括号
括号分为两个运算符 (
和 )
遇到 (
说明会往下走, 所以只需将 (
压栈
遇到 )
说明会往上走, 所以要计算括号表示的子树的结果, 所以要逆向计算运算符直至遇到 (
普通二元运算符
如果当前运算符优先级比上一运算符高, 说明是往下走, 则只需将运算符压栈
如果当前运算符优先级比上一运算符低, 说明是往上走, 则需要一直计算上一运算符直至当前运算符优先级比上一运算符高
3. 核心代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for(int i = 0; i < s.size(); i++) { char c = s[i]; // c 是当前字符 if(isdigit(c)) // 如果 c 是数字, 就提取数字 { int x = 0, j = i; while(j < s.size() && isdigit(s[j])) x = x * 10 + s[j++] - '0'; i = j - 1; num.push(x); } else if(c == '(') op.push(c); // 如果 c 是 '(', 就压栈 else if(c == ')') // 如果 c 是 ')', 就一直计算到 '(' { while(op.top() != '(') eval(); // eval 函数的功能是计算上一运算符 op.pop(); // '(' 出栈 } else // 如果 c 是普通运算符, 就一直计算到 c 的优先级比上一运算符高 { while(op.size() && pr[op.top()] >= pr[c]) eval(); op.push(c); // c 入栈 } }
四. 算法证明
循环不变式
当前运算符及之前所有运算符的左子树 都是形如下图的形状
前五个例子, 后面的形状以此类推
证明
初始化
当 i = 0
时, 当前运算符为空, 循环不变式显然成立
保持
假定在某轮循环前, 循环不变式成立
执行该轮 for
循环
c 为当前字符
当 c
为数字时
会提取数字, 然后将数字压栈
则前面的所有子树形状不变, 循环不变式成立
当 c
为 (
时
会将 (
压栈
则前面的所有子树形状不变, 循环不变式成立
当 c
为 )
时
会一直逆序计算运算符直到遇到 (
根据假定, )
前面的左子树, 即 ()
表示的整棵子树是形如下图的形状
则一直逆序计算运算符直到遇到 (
会计算 ()
表示的整棵子树, 然后将计算结果作为新的节点代替原来的子树
则 ()
表示的整棵子树被一个节点代替, 该节点会使得左子树的形状保持循环不变式的形状
例 1 当 () 表示的整棵子树是上图的第一棵子树时
如 2 + (3) , (3) 就是上图的第一棵子树
计算结果是 2 + 3, 对应的子树保持循环不变式的形状
例 2 当 () 表示的整棵子树是上图的第一棵子树时
如 2 * (3 + 2), (3 + 2) 就是上图的第二棵子树
计算结果是 2 * 5, 对应的子树保持循环不变式的形状
例 3 当 () 表示的整棵子树是上图的第三棵子树时
如 1 + 2 * (3 + 2 * 3) * 3, (3 + 2 * 3) 就是上图的第三棵子树
计算结果是 1 + 2 * 9 * 3, 对应的子树保持循环不变式的形状
注: 此处 “对应的子树” 意为 ‘)’ 运算符前面的运算符的左子树, 即 1 + 2 * 9
当 c
为普通运算符时
会一直逆序计算优先级比自己高的运算符
根据假定, 当前运算符前面的左子树都是形如下图的形状
整棵树的形状可以为下图
一直逆序计算优先级比自己高的运算符会计算当前运算符的左子树, 并将计算结果作为当前运算符的左孩子
过程如下所示
所以当前循环结束后, 循环不变式保持成立
终止
当 for
循环结束后, 整棵树的形状也是循环不变式的形状
总结
所以在 for
循环结束后, 需要逆序计算运算符直至没有运算符为止, 最终整棵树计算为一个节点, 该节点就是表达式的计算结果
队列
模板
普通队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int q[N], hh = 0 , tt = -1 ;q[ ++ tt] = x; hh ++ ; q[hh]; if (hh <= tt){}
循环队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int q[N], hh = 0 , tt = 0 ;q[tt ++ ] = x; if (tt == N) tt = 0 ;hh ++ ; if (hh == N) hh = 0 ;q[hh]; if (hh != tt){}
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;int m;int q[N], hh = 0 , tt = -1 ;int main () { cin >> m; while (m --){ int x; string op; cin >> op; if (op == "push" ){ cin >> x; q[++ tt] = x; } else if (op == "pop" ) hh ++; else if (op == "empty" ) cout << (hh <= tt ? "NO" : "YES" ) << endl; else if (op == "query" ) cout << q[hh] << endl; } return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def main (): n = int (input ()) q = [] while n: data = input ().split() if data[0 ] == "push" : q.append(data[1 ]) elif data[0 ] == "pop" : q.pop(0 ) elif data[0 ] == "empty" : print ("NO" if len (q) else "YES" ) elif data[0 ] == "query" : print (q[0 ]) n -= 1 if __name__ == "__main__" : main()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def main (): n = int (input ()) q = [] hh, tt = 0 , -1 while n: data = input ().split() if data[0 ] == "push" : q.append(data[1 ]) tt += 1 elif data[0 ] == "pop" : hh += 1 elif data[0 ] == "empty" : print ("NO" if hh <= tt else "YES" ) elif data[0 ] == "query" : print (q[hh]) n -= 1 if __name__ == "__main__" : main()
Deque简介
双向队列deque是栈和队列的一种广义实现,是类似于list的容器,可以快速的在队列头部和尾部添加、删除元素,deque是python的collections中的一个类。
append()就是默认从尾部即右侧添加数据
appendleft()方法从deque队列的左侧添加数据
pop() 方法弹出元素,从尾部弹出,并且返回弹出的这个元素
popleft() 方法弹出元素,从头部弹出,并且返回弹出的这个元素、
在队列两端插入或删除元素时间复杂度都是 O(1) ,而在列表的开头插入或删除元 素的时间复杂度为 O(N) 。
deuqe与list的区别相比于list实现的队列,deque实现拥有更低的时间和空间复杂度。list实现出队(pop)和插入(insert)时的空间复杂度大约为O(n),deque在出队(pop)和入队(append)时的时间复杂度是O(1)。
使用q=deque()代替q=list(),因为q.popleft()效率比q.pop(0)高这是因为:列表实现是基于数组的。pop(0)从列表中删除第一个项,它需要左移len(lst) - 1个项来填补空白。 deque()实现使用双向链表。因此无论deque有多大,deque.popleft()都需要一个常量的操作数。即deque.popleft():T(n)=O(1),而list.pop(0):T(n)=O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from collections import dequedef main (): n = int (input ()) q = deque([]) while n: data = input ().split() if data[0 ] == "push" : q.append(data[1 ]) elif data[0 ] == "pop" : q.popleft() elif data[0 ] == "empty" : print ("NO" if q else "YES" ) elif data[0 ] == "query" : print (q[0 ]) n -= 1 if __name__ == "__main__" : main()
单调栈
类似于双指针算法,使用单调栈进行优化时可以首先写出暴力算法,然后研究状态之间的关系,挖掘出某些性质,使我们可以聚焦在比较少的状态中,排除冗余和非法元素/状态,得到单调性并利用单调性得到极值用于求解,实现实现从$O(k)$到$O(1)$的优化。
单调栈的定义
单调栈是栈的一中特殊形式,在栈中的元素必须满足单调性(一定是单调上升或单调下降等等的规律)。
单调栈的思想是利用不断迭代的方式,将当前的元素x与栈顶元素进行比较,按照单调性性质来决定是对栈顶元素做出栈操作还是将当前元素压栈来保证栈的单调性。
单调栈的维护
既然我们必须让元素满足单调性,那么每次插入就和栈顶作比较。如果不满足某些性质,直接弹出栈顶,直到栈为空或满足该性质插入这个元素。
单调栈的性质
单调栈的时间复杂度是 $O(n)$
对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己大的元素),此时栈顶元素就是数组中左侧第一个比自己小的元素;
对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己小的元素),此时栈顶元素就是数组中左侧第一个比自己大的元素;
单调栈的应用
给定一个序列,求序列中的每一个数左边或右边第一个 比他大或比他小的数在什么地方
模板
1 2 3 4 5 6 int tt = 0 ;for (int i = 1 ; i <= n; i ++ ){ while (tt && check (stk[tt], i)) tt -- ; stk[ ++ tt] = i; }
1 2 3 for (int i = 0 ; i < n; i ++) for (int j = i - 1 ; j >= 0 ; j --)
接下来可以思考,在遍历过程中,哪些状态是非法或冗余的:
在数组中,如果i < j
且a[i] > a[j]
,则在j
之后时,显然a[j]
一定能完全替换掉a[i]
(更偏左且更小),a[i]
永远不会被用到,是冗余的。
为了去除冗余,显然我们需要维护一个单调序列,从而保证每个状态都不是冗余的。下面考虑如何维护,这需要考虑题目要求。由于我们找的是最左的一个小于当前数的值,即对于小于当前数的值,最左的优先,而不是最小的优先。因此除了需要维护单调序列之外,还需要保证序列中每个数的相对位置是从左到右排的,且优先取后加入的数作为答案,这显然是可以使用单调栈进行维护。
不妨考虑当前数a[i]
小于s[end]
,则需要用a[i]
替换掉s[end]
。然而这没有结束,还要继续和s
中其他数进行比较,直到整个序列都是单调的。只不过,由于必须保证相对位置是从左到右排的,就必须从右往左单向逐个比较,这即单调栈的出栈入栈过程。
注意要先进行所有出栈操作,再得到当前答案,最后再将当前数入栈。栈为空说明无解。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;int n;int tt;int stk[N];int main () { scanf ("%d" , &n); while (n --){ int x; scanf ("%d" , &x); while (tt && stk[tt] >= x) tt --; if (!tt) printf ("-1 " ); else printf ("%d " , stk[tt]); stk[++ tt] = x; } return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def main (): n = int (input ()) nums = map (int , input ().split()) stk = [] res = [] for x in nums: while stk and x <= stk[-1 ]: stk.pop() if not stk: res.append("-1" ) else : res.append(str (stk[-1 ])) stk.append(x) print (" " .join(res)) if __name__ == "__main__" : main()
单调队列
模板
1 2 3 4 5 6 7 int hh = 0 , tt = -1 ;for (int i = 0 ; i < n; i ++ ){ while (hh <= tt && check_out (q[hh])) hh ++ ; while (hh <= tt && check (q[tt], i)) tt -- ; q[ ++ tt] = i; }
滑动窗口中的最值。
思路也是先写暴力算法,然后研究状态之间的关系,挖掘出某些性质,使我们可以聚焦在比较少的状态中,排除冗余和非法元素/状态,是序列始终是单调的,并利用单调性优化问题。求最值的话直接取端点即可,如果查找一个值的话,可以用二分。
窗口由于元素有进有出,且始终在一侧进另一侧出,因此可以用队列来维护。
研究队列中元素的关系,可以发现由于出队的顺序为先进先出,所以,如果后入队的元素优于先进的元素,在其入队后就完全替代了先入队的元素。本题求的是队列/窗口中的最小值,因此如果a[i] >= a[j]
,且 i < j
(a[i]
会先出队),则a[j]
入队后就替代a[i]
。因此可以直接使冗余元素在a[j]
入队同时出队,从而使队列中序列始终是单调递增的。从而最小值答案就在队头。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> using namespace std;const int N = 1000010 ;int a[N], q[N];int main () { int n, k; scanf ("%d%d" , &n, &k); for (int i = 0 ; i < n; ++ i) scanf ("%d" , &a[i]); int hh = 0 , tt = -1 ; for (int i = 0 ; i < n; ++ i){ if (hh <= tt && i - k + 1 > q[hh]) hh ++; while (hh <= tt && a[q[tt]] >= a[i]) tt --; q[++ tt] = i; if (i >= k - 1 ) printf ("%d " , a[q[hh]]); } puts ("" ); hh = 0 , tt = -1 ; for (int i = 0 ; i < n; ++ i){ if (hh <= tt && i - k + 1 > q[hh]) hh ++; while (hh <= tt && a[q[tt]] <= a[i]) tt --; q[++ tt] = i; if (i >= k - 1 ) printf ("%d " , a[q[hh]]); } puts ("" ); return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def main (): N = 1000010 hh, tt = 0 , - 1 q = [0 ] * N n, k = map (int , input ().split()) nums = list (map (int , input ().split())) for i in range (n): while hh <= tt and i - k + 1 > q[hh]: hh += 1 while hh <= tt and nums[q[tt]] >= nums[i]: tt -= 1 tt += 1 q[tt] = i if i >= k - 1 : print (nums[q[hh]], end=" " ) print () hh, tt = 0 , -1 for i in range (n): while hh <= tt and i - k + 1 > q[hh]: hh += 1 while hh <= tt and nums[q[tt]] <= nums[i]: tt -= 1 tt += 1 q[tt] = i if i >= k - 1 : print (nums[q[hh]], end=" " ) print () if __name__ == "__main__" : main()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from collections import dequedef main (): q= deque([]) n, k = map (int , input ().split()) nums = list (map (int , input ().split())) for i in range (n): while q and i - k + 1 > q[0 ]: q.popleft() while q and nums[q[-1 ]] >= nums[i]: q.pop() q.append(i) if i >= k - 1 : print (nums[q[0 ]], end=" " ) print () q.clear() hh, tt = 0 , -1 for i in range (n): while q and i - k + 1 > q[0 ]: q.popleft() while q and nums[q[-1 ]] <= nums[i]: q.pop() q.append(i) if i >= k - 1 : print (nums[q[0 ]], end=" " ) print () if __name__ == "__main__" : main()
KMP算法
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 for (int i = 2 , j = 0 ; i <= m; i ++ ){ while (j && p[i] != p[j + 1 ]) j = ne[j]; if (p[i] == p[j + 1 ]) j ++ ; ne[i] = j; } for (int i = 1 , j = 0 ; i <= n; i ++ ){ while (j && s[i] != p[j + 1 ]) j = ne[j]; if (s[i] == p[j + 1 ]) j ++ ; if (j == m){ j = ne[j]; } }
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> using namespase std;const int N = 10010 , M = 100010 ;int n, m;char p[N], s[M];int ne[N];int main () { cin.tie (0 ); cin >> n >> p + 1 >> m >> s + 1 ; ne[1 ] = 0 ; for (int i = 2 , j = 0 ;i <= n; i++){ while (j && p[i] != p[j + 1 ]) j = ne[j]; if (p[i] == p[j + 1 ]) j++; ne[i] = j; } for (int i = 1 , j = 0 ; i <= m; i++){ while (j && s[i] != p[j+1 ]) j = ne[j]; if (s[i] == p[j+1 ]) j++; if (j == n){ printf ("%d " , i - n); j = ne[j]; } } return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def main (): n = int (input ()) p = ' ' + input () m = int (input ()) s = ' ' + input () ne = [0 ] * (n + 1 ) j = 0 for i in range (2 , n + 1 ): while j and p[i] != p[j + 1 ]: j = ne[j] if p[i] == p[j + 1 ]: j += 1 ne[i] = j j = 0 for i in range (1 , m + 1 ): while j and s[i] != p[j + 1 ]: j = ne[j] if s[i] == p[j + 1 ]: j += 1 if j == n: print (i - j, end=" " ) j = ne[j] if __name__ == "__main__" : main()
Tire树
基本用法:高效地存储和查找字符串集合 地数据结构
可以查找是否出现过及出现过多少次。字符串末尾符号对应节点要打上标记,说明此字符串存在及出现次数。
利用树形结构共享存储
Tire树一般一定会限制字符范围是26个小写字母或52个大小写字母,或是很小范围内的数字
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int son[N][26 ], cnt[N], idx;void insert (char *str) { int p = 0 ; for (int i = 0 ; str[i]; i ++ ){ int u = str[i] - 'a' ; if (!son[p][u]) son[p][u] = ++ idx; p = son[p][u]; } cnt[p] ++ ; } int query (char *str) { int p = 0 ; for (int i = 0 ; str[i]; i ++ ){ int u = str[i] - 'a' ; if (!son[p][u]) return 0 ; p = son[p][u]; } return cnt[p]; }
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std;const int N = 100010 ;char str[N];int son[N][26 ], cnt[N], idx;void insert (char str[]) { int p = 0 ; for (int i = 0 ; str[i]; ++i){ int u = str[i] - 'a' ; if (!son[p][u]) son[p][u] = ++idx; p = son[p][u]; } ++ cnt[p]; } int query (char str[]) { int p = 0 ; for (int i = 0 ; str[i]; ++i){ int u = str[i] - 'a' ; if (!son[p][u]) return 0 ; p = son[p][u]; } return cnt[p]; } int main () { int n; scanf ("%d" , &n); while (n--){ char op[2 ]; scanf ("%s%s" , op, str); if (op[0 ] == 'I' ) insert (str); else printf ("%d\n" ,query (str)); } return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def main (): N = int (1e5 + 5 ) son = [[0 ] * 26 for _ in range (N)] cnt = [0 ] * N idx = 0 def insert (x ): nonlocal idx p = 0 for v in x: u = ord (v) - ord ('a' ) if not son[p][u]: idx += 1 son[p][u] = idx p = son[p][u] cnt[p] += 1 def query (x ): p = 0 for v in x: u = ord (v) - ord ('a' ) if not son[p][u]: return 0 p = son[p][u] return cnt[p] n = int (input ()) while n: n -= 1 op, s = input ().split() if op == 'I' : insert(s) else : print (query(s)) if __name__ == "__main__" : main()
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 , M = 31 * N;int n;int a[N], son[M][2 ], idx;void insert (int x) { int p = 0 ; for (int i = 30 ; i >= 0 ; -- i){ int &s = son[p][x >> i & 1 ]; if (!s) s = ++ idx; p = s; } } int query (int x) { int p = 0 , res = 0 ; for (int i = 30 ; i >= 0 ; -- i){ int s = x >> i & 1 ; if (son[p][!s]){ res += 1 << i; p = son[p][!s]; } else p = son[p][s]; } return res; } int main () { scanf ("%d" , &n); for (int i = 0 ; i < n; ++ i){ scanf ("%d" , &a[i]); insert (a[i]); } int res = 0 ; for (int i = 0 ; i < n; ++ i) res = max (res, query (a[i])); printf ("%d\n" , res); return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def main (): N = 100010 M = N * 31 son = [[0 ] * 2 for _ in range (M)] idx = 0 def insert (x ): nonlocal idx p = 0 for i in range (30 , -1 , -1 ): u = x >> i & 1 if not son[p][u]: idx += 1 son[p][u] = idx p = son[p][u] def query (x ): p, res = 0 , 0 for i in range (30 , -1 , -1 ): u = x >> i & 1 if son[p][~u]: res += 1 << i p = son[p][~u] else : p = son[p][u] return res n = int (input ()) nums = list (map (int , input ().split())) res = 0 for x in nums: insert(x) res = max (query(x), res) print (res) if __name__ == "__main__" : main()
并查集
操作:
将两个集合合并
询问两个元素是否在一个集合当中
并查集能够在近乎$O(1)$完成这两个操作
原理:
每个集合用一棵树表示,树根的编号就是集合的编号
每个节点存储它的父节点,p[x]表示x的父节点
技巧:
如何判断树根:== x)
如何求x的集合编号:!= x) x = p[x]
如何合并两个集合: px是x的集合编号,py是y的集合编号,只需= y (合并集合只需要增加一个从根节点到根节点的边)
优化:
一般使用“路径压缩”,而“按秩合并”一般可不使用
并查集同时可以维护
模板
递归路径压缩
1 2 3 4 5 6 int find (int x) { if (x != p[x]){ p[x] = find (p[x]); } return p[x]; }
非递归式
查两遍,第一遍找到祖宗节点并存储,第二遍更新路径上每个节点的父节点为祖宗节点
1 2 3 4 5 6 7 8 9 10 11 12 13 int find (int x) { int k, j, r; r = x; while (r != p[r]) r = p[r]; k = x; while (k != r){ j = p[k]; p[k] = r; k = j; } return r; }
(1) 朴素并查集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int p[N]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } for (int i = 1 ; i <= n; i ++ ) p[i] = i;if (find (a) == find (b))p[find (a)] = find (b);
(2)维护集合大小size的并查集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int p[N], size[N];int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } for (int i = 1 ; i <= n; i ++ ){ p[i] = i; size[i] = 1 ; } size[find (b)] += size[find (a)]; p[find (a)] = find (b);
(3)维护到祖宗节点距离的并查集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int p[N], d[N];int find (int x) { if (p[x] != x){ d[x] += d[p[x]]; p[x] = find (p[x]); } return p[x]; } for (int i = 1 ; i <= n; i ++ ){ p[i] = i; d[i] = 0 ; } p[find (a)] = find (b); d[find (a)] = distance;
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> using namespace std;const int N = 100010 ;int n, m;int p[N];int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } int main () { scanf ("%d%d " , &n, &m); for (int i = 1 ; i <= n; i++) p[i] = i; while (m --) { char op[2 ]; int a, b; scanf ("%s%d%d" , op, &a, &b); if (op[0 ] == 'M' ) p[find (a)] = find (b); else { if (find (a) == find (b)) puts ("Yes" ); else puts ("No" ); } } return 0 ; }
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def main (): N = 100010 p = [0 ] * N def find (x ): if p[x] != x: p[x] = find(p[x]) return p[x] n, m = map (int , input ().split()) for i in range (1 , n + 1 ): p[i] = i while m: m -= 1 op, a, b = input ().split() a, b = int (a), int (b) if op == 'M' : p[find(a)] = p[find(b)] else : if find(a) == find(b): print ("Yes" ) else : print ("No" ) if __name__ == "__main__" : main()
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;int n, m;int p[N], cnt[N];int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } int main () { scanf ("%d%d" , &n, &m); for (int i = 0 ; i < n; ++ i){ p[i] = i; cnt[i] = 1 ; } while (m --){ string op ; int a, b; cin >> op; if (op == "C" ){ cin >> a >> b; if (find (a) != find (b)){ cnt[find (b)] += cnt[find (a)]; p[find (a)] = find (b); } } else if (op == "Q1" ){ cin >> a >> b; if (find (a) == find (b)) puts ("Yes" ); else puts ("No" ); } else { cin >> a; printf ("%d\n" , cnt[find (a)]); } } return 0 ; }
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 def main (): N = 100010 p = [0 ] * N cnt = [0 ] * N def find (x ): if p[x] != x: p[x] = find(p[x]) return p[x] n, m = map (int , input ().split()) for i in range (1 , n + 1 ): p[i] = i cnt[i] = 1 while m: m -= 1 op, *num = input ().split() if op == 'C' : a, b = int (num[0 ]), int (num[1 ]) a, b = find(a), find(b) if a != b: cnt[b] += cnt[a] p[a] = b elif op == 'Q1' : a, b = int (num[0 ]), int (num[1 ]) if find(a) == find(b): print ("Yes" ) else : print ("No" ) else : a = int (num[0 ]) print (cnt[find(a)]) if __name__ == "__main__" : main()
堆
堆即优先队列
操作:
插入一个数
1 heap[++ size] = x; up (size)
求集合当中的最小值
删除最小值
1 2 3 heap[1 ] = heap[size]; size--; down (1 );
删除任意一个元素
1 2 3 4 heap[k] = heap[size]; size --; down (k);up (k);
修改任意一个元素
1 2 3 heap[k] = x; down (k);up (k);
堆是完全二叉树,除了最后一层节点都是满的
小根堆:父节点小于两个子节点
大根堆:父节点大于两个子节点
模拟堆:使用数组存储二叉树
x左儿子:2x
x右儿子:2x+1
删除元素时之所以需要用最后一个元素替换待删除元素是因为数组模拟时显然最后一个位置方便删除而之前的位置不容易删除。所以采用替换删除的策略。
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 int h[N], ph[N], hp[N], size;void heap_swap (int a, int b) { swap (ph[hp[a]],ph[hp[b]]); swap (hp[a], hp[b]); swap (h[a], h[b]); } void down (int u) { int t = u; if (u * 2 <= size && h[u * 2 ] < h[t]) t = u * 2 ; if (u * 2 + 1 <= size && h[u * 2 + 1 ] < h[t]) t = u * 2 + 1 ; if (u != t){ heap_swap (u, t); down (t); } } void up (int u) { while (u / 2 && h[u] < h[u / 2 ]){ heap_swap (u, u / 2 ); u >>= 1 ; } } for (int i = n / 2 ; i; i -- ) down (i);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n, m;int h[N], cnt;void down (int u) { int t = u; if (u * 2 <= cnt && h[u*2 ] < h[t]) t = u * 2 ; if (u * 2 + 1 <= cnt && h[u*2 +1 ] < h[t]) t = u * 2 + 1 ; if (u != t ){ swap (h[u], h[t]); down (t); } } void up (int u) { while (u / 2 && h[u/2 ] > h[u]){ swap (h[u/2 ], h[u]); u /= 2 ; } } int main () { scanf ("%d%d" , &n,&m); for (int i = 1 ;i <= n; ++i) scanf ("%d" , &h[i]); cnt = n; for (int i = n / 2 ; i; -- i) down (i); while (m--){ printf ("%d " , h[1 ]); h[1 ] = h[cnt--]; down (1 ); } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n, m;int h[N], ph[N], hp[N], cnt;void heap_swap (int a, int b) { swap (ph[hp[a]], ph[hp[b]]); swap (hp[a], hp[b]) } void down (int u) { int t = u; if (u*2 <= cnt && h[u*2 ] < h[t]) t = u * 2 ; if (u*2 +1 <= cnt && h[u*2 +1 ] < h[t]) t = u * 2 + 1 ; if (u != t ){ swap (h[u], h[t]); down (t); } } void up (int u) { while (u / 2 && h[u/2 ] > h[u]){ swap (h[u/2 ], h[u]); u /= 2 ; } } int main () { scanf ("%d%d" , &n,&m); for (int i = 1 ;i <= n; ++i) scanf ("%d" , &h[i]); cnt = n; for (int i = n / 2 ; i;--i) down (i); while (m--){ printf ("%d " , h[1 ]); h[1 ] = h[cnt--]; down (1 ); } return 0 ; } int dist[N], backup[N];struct Edge { int a, b, w; }edges[M]; void bellman_ford () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < k; ++ i){ memcpy (backup, dist, sizeof dist); for (int j = 0 ; i < m; ++ j){ int a = edges[j].a, b = edges[j].b, w = edges[j].w; dist[b] = min (dist[b], backup[a] + w); } } } int h[N], w[N], e[N], ne[N], idx;int dist[N];bool st[N];void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++; }
哈希表
哈希表的作用:把庞大复杂的数据映射到较小的范围。通常的范围是0到$105$或$10 6$
$h(x)$ 可以把数据进行映射,称为哈希函数。我们可以这么考虑:
$\large{h(x)=x\ \text{mod}\ 10003}$ 。
时间复杂度 $O(1)$ ,空间复杂度 $O(n)$
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 (1 ) 拉链法 int h[N], e[N], ne[N], idx; void insert (int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++ ; } bool find (int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1 ; i = ne[i]) if (e[i] == x) return true ; return false ; } (2 ) 开放寻址法 int h[N]; int find (int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0 ; } return t; }
C++
拉链法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 1e5 + 3 ; int h[N], e[N], ne[N], idx; void insert (int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++; } bool find (int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1 ; i = ne[i]) if (e[i] == x) return true ; return false ; } int main () { scanf ("%d" , &n); memset (h, -1 , sizeof h); while (n --){ string op; int x; cin >> op >> x; if (op == "I" ) insert (x); else { if (find (x)) puts ("Yes" ); else puts ("No" ); } } return 0 ; }
开放地址法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 2e5 + 3 ; const int null = 0x3f3f3f3f ; int n;int h[N];int find (int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x){ t ++; if (t == N) t = 0 ; } return t; } int main () { scanf ("%d" , &n); memset (h, 0x3f , sizeof h); while (n --){ string op; int x; cin >> op >> x; if (op == "I" ) h[find (x)] = x; else { if (h[find (x)] == null) puts ("No" ); else puts ("Yes" ); } } return 0 ; }
Python
拉链法
str.isalpha()实现的是如果字符串至少有一个字符并且所有字符都是字母则返回 True,否则返回 False
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 def main (): MAX_N = int (1e5 + 3 ) h, e, ne = [-1 ] * MAX_N, [0 ] * MAX_N, [0 ] * MAX_N idx = 0 def add (x ): nonlocal idx k = (x % MAX_N + MAX_N) % MAX_N e[idx] = x ne[idx] = h[k] h[k] = idx idx += 1 def contains (x ): k = (x % MAX_N + MAX_N) % MAX_N ptr = h[k] while ptr != -1 : if e[ptr] == x: return True ptr = ne[ptr] return False n = int (input ()) for _ in range (n): op, x = map (lambda x : x if x.isalpha() else int (x), input ().split()) if op == 'I' : add(x) else : print ("Yes" if contains(x) else "No" ) if __name__ == "__main__" : main()
开放寻址法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def main (): MAX_N = int (2e5 + 3 ) hs = [None ] * MAX_N def find (x ): pos = (x % MAX_N + MAX_N) % MAX_N while hs[pos] != x and hs[pos] != None : pos = (pos + 1 ) % MAX_N return pos n = int (input ()) for _ in range (n): op, x = map (lambda x : x if x.isalpha() else int (x), input ().split()) if op == 'I' : hs[find(x)] = x else : print ("Yes" if hs[find(x)] != None else "No" ) if __name__ == "__main__" : main()
字符串哈希
字符串哈希用于快速比较两个子串是否完全相同,复杂度为$O(n)+O(m)$ ,原理本质上是前缀哈希+前缀和 。
全称字符串前缀哈希法,把字符串变成一个 p 进制数字(哈希值),实现不同的字符串映射到不同的数字。对形如 $X_1X_2X_3\cdots X_{n-1}X_n$ 的字符串, 采用字符的 ASCII 码乘上 P 的次方来计算哈希值。
映射公式
$$
(X_1 \times P^{n-1} + X_2 \times P^{n-2} + \cdots + X_{n-1} \times P^1 + X_n \times P^0) \bmod Q
$$
注意点:
任意字符不可以映射成 0,否则会出现不同的字符串都映射成 0 的情况,比如 A,AA,AAA 皆为 0
冲突问题:通过巧妙设置 P (131 或 13331) , Q $(2^{64})$ 的值,一般可以理解为不产生冲突。
M=998244353
问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。
前缀和公式:
$$
h[i+1] = h[i] \times P + s[i], \ i \in [0,n-1]
$$
h 为前缀和数组,s 为字符串数组
区间和公式:
$$
h[l,r] = h[r] - h[l-1] \times P^{r-l+1}
$$
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,乘上 $P^2$ 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 核心思想:将字符串看成P进制数,P的经验值是131 或13331 ,取这两个值的冲突概率低 小技巧:取模的数用2 ^64 ,这样直接用unsigned long long 存储,溢出的结果就是取模的结果 typedef unsigned long long ULL;ULL h[N], p[N]; p[0 ] = 1 ; for (int i = 1 ; i <= n; i ++ ){ h[i] = h[i - 1 ] * P + str[i]; p[i] = p[i - 1 ] * P; } ULL get (int l, int r) { return h[r] - h[l - 1 ] * p[r - l + 1 ]; }
判断任意两个区间包含的子串是否完全相同
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <cstring> #include <algorithm> using namespace std;typedef unsigned long long ULL;const int N = 100010 , P = 131 ;int n, m;char str[N];ULL h[N], p[N]; ULL get (int l, int r) { return h[r] - h[l - 1 ] * p[r - l + 1 ]; } int main () { scanf ("%d%d" , &n, &m); scanf ("%s" , str); p[0 ] = 1 ; for (int i = 1 ; i <= n; ++ i){ p[i] = p[i - 1 ] * P; h[i] = h[i - 1 ] * P + str[i - 1 ]; } while (m --){ int l1, r1, l2, r2; scanf ("%d%d%d%d" , &l1, &r1, &l2, &r2); if (get (l1, r1) == get (l2, r2)) puts ("Yes" ); else puts ("No" ); } return 0 ; }
Python
C++ 可以使用 unsigned long long
自动溢出来对 (1 << 64) - 1
取模,Python / Java 需手动取模并注意取模时的括号范围。
内置函数 ord ()用于返回字符的ASCII码用法:ord('C')
参数是一个字符,不能是字符串,返回该字符对应的Unicode码
对应的还是有ord()函数的配对函数chr()
chr()的作用是:输入一个十进制或十六进制数字,返回其在Unicode编码中对应的文字或符号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def main (): P, MOD = 131 , (1 << 64 ) n, m = map (int , input ().split()) hs, bs = [0 ] * (n + 1 ), [1 ] * (n + 1 ) def get (l, r ): return (hs[r] - hs[l - 1 ] * bs[r - l + 1 ]) % MOD s = input () for i in range (1 , n + 1 ): bs[i] = (bs[i - 1 ] * P) % MOD hs[i] = (hs[i - 1 ] * P + ord (s[i - 1 ])) % MOD for _ in range (m): l1, r1, l2, r2 = map (int , input ().split()) print ("Yes" if get(l1, r1) == get(l2, r2) else "No" ) if __name__ == "__main__" : main()