快速排序
快速排序是基于分治思想的一种排序方法, 同时也是一种不稳定的算法。
假设arr为要排序的数组。
- 确定排序区间分界点:可以为arr[left] 、 arr[(left + right) / 2] 、 arr[right]或者随机取一个数作为分界点。
- 调整区间: 小于分界点的放在数组的左边, 大于分界点的放在数组的右边。
- 然后递归处理分好的两个区间。
其中最重要的就是第二步的处理:首先定义两个指针i, j, 分别指向数组的第一个数和最后一个数, 然后我们取数组第一个数作为分界点,分别移动左右指针向中间靠拢。 当i指向的那个数大于分界点时,停止移动i,转而移动j, 如果j指向的值小于分界点, 那么交换i和j指向的值, 再然后就是不断递归此操作。
def quicksort(arr, l, r):
if l >= r:
return
left = l
right = r
pivot = arr[left]
while left < right:
while left < right and arr[right] >= pivot:
right -= 1
if left < right:
arr[left] = arr[right]
while left < right and arr[left] <= pivot:
left += 1
if left < right:
arr[right] = arr[left]
if left >= right:
arr[left] = pivot
quicksort(arr, l, right - 1)
quicksort(arr, left + 1, r)
return arr
归并排序
归并排序的基本思想还是分治,速度较慢与快速排序,但它是一种稳定的排序算法。
基于分治思想的归并排序, 在每层递归中都有三个步骤:
- 找到分界点, 将数组一分为二, mid = (left + right) / 2
- 把数组中心作为分界点后, 然后把数组两边的数分别进行排序大小。
- 最后把两边排序好的数组再合并起来, 按照从小到大的次序。
第三部为此算法的核心(我采用的是双指针算法解决): 对两边的数递归完后有两个排序好了的数组,在两个数组的头一个位置各插入一个指针, 两个指针不断遍历比较, 较小的数加入到新数组中。
def merge_sort(arr, l, r, tmp):
if l >= r: # 如果只有一个数 则直接退出函数
return
mid = (l + r) // 2 # 找到中间点
merge_sort(arr, l, mid, tmp) # 递归两边 不断分成更小的子问题
merge_sort(arr, mid + 1, r, tmp)
# 回溯合并排序好的子问题
res, k, i, j = [], 0, l, mid + 1
while i <= mid and j <= r: # 比较排序好的两个数组, 比较两个指针所指的数的大小,较小的加进tmp数组,然后指向下一个
if arr[i] <= arr[j]:
tmp[k] = arr[i]
i += 1
else:
tmp[k] = arr[j]
j += 1
k += 1
while i <= mid: # 如果一个数组的指针已经走完了,那么另一个数组的剩余值直接加入到tmp数组即可
tmp[k] = arr[i]
k += 1
i += 1
while j <= r:
tmp[k] = arr[j]
k += 1
j += 1
i, j = l, 0
while i <= r: # 将tmp数组的值复制进arr数组
arr[i] = tmp[j]
i += 1
j += 1
return arr
二分法
二分法是一个非常重要的算法,也是一个十分基础的算法, 通俗的讲,就是通过一分为二的方法找到你所给数据中的答案。 例如你要在1-100中猜一个数字, 如果你是从1一直猜到100, 你可能要猜100次,但如果你使用二分法猜, 至多只需要7次, 这些数据可能也不是那么震撼, 那假如是4亿的数据范围呢,使用二分法最多只需要查找32次。这就足够说明二分法的优势了。
现在介绍整数如何进行二分操作:
- 先确定数组内数据是否有序, 无序则排序一遍
- 整个区间一分为二, 两边的数据分别满足某种性质,例如查找1-100内的数,假如要找的数是11, 那么第一次一分为二时,分界点为50, 此时50右边的数据满足能查找到这个数的性质, 则第一个区间的右端点可以重新设置为49,50右边的数不满足能查找到这个数的性质,就舍弃掉。同理,假如这个数在分界点右边的话,那就把左端点设置为51.
- L|----------||----------|R
二分法的重点就是左右半部分如何划分:
左半部分讨论
mid = (l + r) / 2
if check(mid) == true:
答案在 [mid, r] 更新方式 l = mid
(if check(mid) == false:
答案在[l, mid - 1] 更新方式r = mid - 1)
右半部分讨论
mid = (l + r + 1)
if check(mid) == true:
答案在[l, mid] 更新方式 r = mid
(if check(mid) == false:
答案在[mid + 1, r] 更新方式l = mid + 1)
然后就是整个的查找过程:
n, q = map(int, input().split())
arr = list(map(int, input().split()))
while q:
q -= 1
x = int(input()) # 读入要查找的数
l = 0
r = n - 1
while l < r:
mid = l + r >> 1
if arr[mid] >= x:
r = mid
else:
l = mid + 1
if arr[l] != x: # 如果要查找的数不在数组中,则只要判断l指针所指向的值是否等于x即可
print('-1 -1') # 因为l会不断向后遍历直到找到x的值为止
else:
print(l, end = ' ')
l = 0
r = n - 1
while l < r:
mid = l + r + 1 >> 1
if arr[mid] <= x:
l = mid
else:
r = mid - 1
print(l)
高精度算法(python其实可以不用学习此内容, 因为python的特性导致)
高精度加法
高精度加法的实现过程就是小学学习加法时的实现过程,同位数相加和大于10就向前进一。
首先我们用两个数组把要相加的数存储起来(为方便计算), 采用倒序存储, 把数的低位从数组的头开始存储。 然后我们用t来代表是否进位, 初始化为0, 因为个位数并不能被进一位。 个位相加,小于10, 就存储在C这个新数组中,大于10,则在C中存储当前位数相加和的余数,然后t变成相加后的数的整除数, 下一位数相加时, 加上这个进位数即可。
def add(a, b):
C = []
t = 0
lenth_a = len(a)
lenth_b = len(b)
if lenth_a > lenth_b:
lenth = lenth_a
else:
lenth = lenth_b
for i in range(lenth):
if i <= lenth_a - 1:
t += a[i]
if i <= lenth_b - 1:
t += b[i]
C.append(t % 10)
t = t // 10
if t:
C.append(1)
return C
if __name__ == "__main__":
str_lista = list(input())[::-1] # 将两个数字逆序存入列表中
str_listb = list(input())[::-1]
A, B = [], []
for i in str_lista: # 将列表中的元素转为整数
A.append(int(i))
for j in str_listb:
B.append(int(j))
C = add(A, B) # 调用写好的add函数
for i in C[::-1]: # 逆序输出相加好的整数的逆序列表
print(i, end='')
高精度减法
高精度减法就和小学减法一样, 相同位数的数相减, 要是不够减,则向前借一。下一位计算时减去一即可。(要注意的是, 减法的实现过程要保证是大数减小数) 下面的实现过程中cmp()函数就是发挥比较两数大小作用的。
def cmp(a, b):
if len(a) != len(b): # 如果两个数字位数不同
return len(a) > len(b) # a > b 时输出True a < b 时输出False
else:
lenth = len(a)
flag = True
for i in range(lenth - 1, - 1, -1): # 如果数字位数相同
if a[i] != b[i]: # 从后往前遍历两个数组
flag = (a[i] > b[i]) # a[i] 比 b[i]大就返回true 否则返回flase
break
return flag
return True
def sub(a, b):
C = []
t = 0
if len(a) > len(b):
lenth = len(a)
else:
lenth = len(b) # 找到最大数字的位数
for i in range(lenth):
t = a[i] - t # 是否向前借1 被借了一位就要减一
if i < len(b): # 如果i还小于b列表的长度
t -= b[i] # 减去b[i]位的数 就是当前位数的减法操作
C.append((t + 10) % 10) # 在新列表中添加当前位数减法操作完后的数
if t < 0: # 更新是否借一位的情况
t = 1
else:
t = 0
while len(C) > 1 and C[-1] == 0: # 删除前导零 例如:003 -> 3
C.pop()
return C
if __name__ == "__main__":
A = list(map(int, input()))[::-1]
B = list(map(int, input()))[::-1]
if cmp(A, B): # 保证是大数字减小数字
C = sub(A, B)
for i in C[::-1]:
print(i, end='')
else:
C = sub(B, A)
print('-', end='')
for i in C[::-1]:
print(i, end='')
高精度乘法
大数乘小数:
# 大数乘小数
def multi(a, b):
t, i = 0, 0
C = []
while t or i < len(a):
if i < len(a):
t += a[i] * b # 把b一整个数乘a中每个位置上的数
i += 1
C.append(t % 10)
t //= 10
while len(C) > 1 and C[-1] == 0: # 删除前导零
C.pop()
return C
if __name__ == "__main__":
a = list(map(int, input().split()))[::-1]
b = int(input())
C = multi(a, b)
for i in C[::-1]:
print(i, end='')
大数乘大数:
def multi(a, b):
t = 0
flag = 0
C = [[0 for j in range(len(a) * len(b) + 10)] for i in range(len(b))] # 构建一个列数为len(a) * len(b) + 10 行数为len(b)的矩阵
# 列数+10 防止遍历超出列表长度
c = [str(i) for i in a] # 判断a, b之中是否为零, 有零存在直接返回[0]
d = [str(k) for k in b]
c = ''.join(c)
d = ''.join(d)
if c == '0' or d == '0':
return [0]
for i in range(len(b)): # 从b的个位数开始与a的每个数进行乘法 然后存储在矩阵的第一行
k = flag # b的个位数与a乘完之后 进位到b的下一位数 然后存储在矩阵的下一行
for j in range(len(a)):
t += (b[i] * a[j])
C[i][k] = (t % 10)
if t >= 10:
t //= 10
else:
t = 0
k += 1
C[i][k] = t
t = 0
flag += 1
t = 0
end = [0] * (len(a) * len(b)) # 将矩阵的每行数进行相加 当有某列相加大于10 矩阵的下一列就要加上上一列相加之和的整除数
for j in range(0, len(a) * len(b)):
for i in range(0, len(b)):
t += C[i][j]
end[j] = (t % 10) # 将结果添加进新列表中
t //= 10
while end[-1] == 0: # 删除前导零
end.pop()
return end
if __name__ == "__main__":
A = list(map(int, input()))[::-1]
B = list(map(int, input()))[::-1]
flag = 0
c = multi(A, B)
for i in c[::-1]: # 从后往前遍历列表 逐一输出 便得到两数相乘结果
print(i, end='')
高精度除法
和小学除法一致。
def div(A, b, r):
C = []
for i in range(len(A) - 1, -1, -1):
r = r * 10 + A[i] # 余数乘与10 加上当前位置上的数即是当前位置的被除数
C.append(r // b) # 新数组加上被除数整除于除数的商
r %= b # 然后得到下一位余数
C = C[::-1] # 和前面的高精度算法不同,除法中的前导零是在商的前面,所以要把商倒置过来才能删去前导零。
while len(C) and C[-1] == 0:
C.pop()
return C[::-1], r # 然后再把商的数组和最后的余数输出即可
if __name__ == "__main__":
A = list(map(int, input()))[::-1] # A为被除数
b = int(input()) # b为除数
r = 0 # r为余数
c, t = div(A, b, r)
for i in c:
print(i, end='')
print('')
print(t)
前缀和
一维数组的前缀和
前缀和就是把数组中每位数的前N项和。 求出每个元素的的前N项和,之后求给出区间的元素和时, 只需求尾区间- 头区间的前缀和就行。
例: arr = [1, 2, 3 , 4, 5, 6, 7]
s = [1, 3, 6, 10, 15, 21, 28]
输入a, b = 1, 4
前N项和 = s[3] - s[0]
N = 100010
A = [0] * N
B = [0] * N
if __name__ == "__main__":
n, m = map(int, input().split())
A[1: n + 1] = list(map(int, input().split()))
for i in range(1, n + 1):
B[i] = B[i - 1] + A[i] # 第i位的前缀和等于前一位的前缀和加上当前位置的值
C = []
while m:
m -= 1
l, r = map(int, input().split())
C.append(B[r] - B[l - 1])
for i in C:
print(i)
二维数组的前缀和
利用了容斥原理计算
例: 求出矩阵每个元素左上角的矩阵之和,s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + arr[i][j]
再根据给出的矩阵左上角坐标和右下角坐标计算要求的子矩阵和
x1, y1, x2, y2为所给子矩阵左上角和右下角的坐标
sum = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]
n, m, q = map(int, input().split())
arr = [[0 for j in range(m + 1)] for i in range(n + 1)]
s = [[0 for j in range(m + 1)] for i in range(n + 1)]
for i in range(1, n + 1):
ret = list(map(int, input().split()))
for j in range(m):
arr[i][j + 1] = ret[j]
for i in range(1, n + 1): # 求解每个元素左上角的矩阵和
for j in range(1, m + 1):
s[i][j] = s[i -1][j] + s[i][j - 1] - s[i - 1][j - 1] + arr[i][j]
while q:
q -= 1
x1, y1, x2, y2 = map(int, input().split()) # 求解给出坐标的子矩阵和
print(s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 -1] + s[x1 -1][y1 -1])
差分法
也利用了容斥定理
一维数组差分法:
把输入的数组当作前缀和数列
然后得出这个数组的差分数列(即得到这个前缀和之前的数组)
使用insert方法 构造出差分数组
如:
insert(l, r, c):
b\[l] += c
b\[r + 1] -= c
a = [1, 3, 5, 6, 7, 4, 9]
b = [1, 2, 2, 1, 1, -3, 12]
a就是b的前缀和数组
b就是a的差分数组
得到a数组的差分数组之后, 只需在差分数组所要操作的区间的进行insert(l, r, c)操作,
就可使那一整个区间的数都进行那个操作(即加上c或者减上c等等)
然后再向b数组进行前缀和求和即可。
N = 100010
b = [0] * N
a = [0] * N
def insert(l, r, c):
b[l] += c
b[r + 1] -= c
if __name__ == "__main__":
n, m = map(int, input().split())
a = list(map(int, input().split()))
for i in range(1, n + 1):
insert(i, i, a[i - 1])
while m:
l, r, c = map(int, input().split())
insert(l, r, c)
m -= 1
for j in range(1, n + 1):
b[j] += b[j - 1]
for j in range(1, n + 1):
print(b[j], end=' ')
二维数组差分法
和一维数组差分法类似。
只不过insert方法变了。
insert(x1, y1, x2, y2):
b[x1][y1] += c
b[x2 + 1][y1] -= c
b[x1][y2 + 1] -= c
b[x2 + 1][y2 + 1] += c
解释:
差分矩阵左上角的第一个数加c或者减c , 那么右下角的矩阵的前缀和都会加上c或者减上c。
然后只需把右下角矩阵中不需要进行这样操作的其他矩阵变回之前的样子就行。
b = [[0 for j in range(N)] for i in range(N)]
a = [[0 for j in range(0, N)] for i in range(0, N)]
def insert(x1, y1, x2, y2, c):
b[x1][y1] += c
b[x2 + 1][y1] -= c
b[x1][y2 + 1] -= c
b[x2 + 1][y2 + 1] += c
if __name__ == "__main__":
n, m, q = map(int, input().split())
for i in range(1, n + 1):
ret = list(map(int, input().split()))
for j in range(m):
a[i][j + 1] = ret[j]
for i in range(1, m + 1):
for j in range(1, n + 1):
insert(i, j, i, j, a[i][j])
while q:
q -= 1
x1, y1, x2, y2, c = map(int, input().split())
insert(x1, y1, x2, y2, c)
for i in range(1, m + 1):
for j in range(1, n + 1):
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]
for i in range(1, n + 1):
for j in range(1, m + 1):
print(b[i][j], end=' ')
print('')
双指针算法
顾名思义,就是利用两个指针,在一个数组或者链表结构中解决相关问题,降低时间复杂度。
如判断最长不重复子序列:
就是遍历数组a中的每一个元素a[i], 对于每一个i,找到j使得双指针[j, i]维护的是以a[i]结尾的最长连续不重复子序列,长度为i - j + 1, 将这一长度与r的较大者更新给r(r为不重复子序列的长度)
if __name__ == "__main__":
n = int(input())
arr = list(map(int, input().split()))
max_lenth = 0
i = 0 # 初始化指针
j = 0
b = [arr[0]] # 把最长不重复序列存入b数列中
while i < n - 1:
while j + 1 <= n - 1 and arr[j] not in b: # 遍历一遍arr 把连续且不重复的数存入到b中
b.append(arr[j])
j += 1
lenth = j - i + 1 # 计算最长不重复子序列的长度
i = j + 1 # 把i指针的位置移到上一段不重复序列的下一个位置
b = []
if lenth > max_lenth: # 更新给出数列中不重复子序列的长度
max_lenth = lenth
print(max_lenth)
位运算
lowbit运算
定义一个lowbit函数, f = lowbit(x), 函数的值是x在二进制表示中最低位所在1所对应的值。
lowbit函数的实现:
- x & -x 例: 当x = 6: 二进制源码为: 110 二进制反码为: 001 二进制补码为: 010 (正数的补码等于它的原码;负数的补码等于反码+1)
由于是反码, 进位之后由于1的作用使进位的部分全部与反码取反即与原码相同,lowbit以前的部分x与其补码相反,x & - x 之后除了lowbit位是1, 其余位都是0,构成了一个1000...的数串。
2.x&(x^(x - 1)) 当对一个二进制数减1时,那么从这个数的最后一个1开始到最后的所有数都会取反,即构成了一个0111...的数串。 我们把这个数与原数进行异或,那么得到的数就会是: 原数第一个1以后的数(包括第一个)全部取1,其他位的数全部取0,构成了一个一堆0后面跟一堆1的串。 我们再把这个数与原数做与运算,那么除了原数第一个1为1, 其他位置全部为0。
例:当x = 6: 原码为 110; 减一后的数为: 101; 与原数异或后的数为: 011; 与原数进行与运算后的为: 010。 最后得到的这个数即是最低位的1所对应的值。
例: 计算二进制数中1的个数:
def count(num):
count = 0 # 计算1的个数
while num:
num -= num & -num # 利用lowbit运算
count += 1
return count
n = int(input())
arr = list(map(int, input().split())) # 输入二进制数
for i in range(n):
print(count(arr[i]), end=' ')
离散化
离散化,就是当我们只关心数据的大小关系时,用排名代替原数据进行处理的一种预处理方法。 在不改变原有序列的大小关系下,将其大小关系映射成正整数。(尤其适用所给数据范围很大,但给的数据很少的情况下)
离散化的步骤: 排序,去重,然后求出映射后的值
alls: 存储所有待离散化的值
sort(alls[start, end]) 将所有值排序
alls = set(alls) 将alls内所有重复元素去除
# 二分化求出x对应的离散化的值
def find(x):
left = 0
right = len(alls) - 1
while left < right:
mid = left + right >> 1
if alls[mid] >= x:
right = mid
else:
left = mid + 1
return right + 1 # right + 1 就是映射成从1开始的整数序列
# 区间和
# 将所给列表的值映射到另外一个列表
# 适用列表范围很大,值很少的情况
def find(x, arr): # 对x进行离散化操作 (二分法)
"""
:param x: # 输入的数要被离散化的列表下标
:param arr: # 输入的是已经去重过的存储操作下标的列表
:return: # 返回从1开始映射的小列表 (即每调用这个函数一次,输出的下标就加一)
"""
l = 0
r = len(arr) - 1
while l < r:
mid = l + r >> 1
if arr[mid] >= x:
r = mid
else:
l = mid + 1
return l + 1 # 返回从1开始映射的值 1 2 3 4 ...
def sort_list(arr):
"""
:param arr: 输入存储操作位置的列表
:return: 输出去除重复操作位置的列表
"""
arrs = {i for i in arr}
arrs = set(arrs)
arrs = [i for i in arrs]
arrs.sort(reverse=False) # 将去除重复元素的列表进行升序排序
return arrs
if __name__ == "__main__":
n, m = map(int, input().split())
addnum = []
alls = []
query = []
while n:
n -= 1
x, c = map(int, input().split())
addnum.append([x, c])
alls.append(x)
while m:
m -= 1
l, r = map(int, input().split())
query.append([l, r])
alls.append(l)
alls.append(r)
alls = sort_list(alls)
# 开始计算映射后的小数组加上c后的值
num = [0] * (len(alls) + 5)
s = [0] * (len(alls) + 5)
for x, c in addnum:
num[find(x, alls)] += c
# 计算映射后加上c的列表的前缀和
for i in range(1, len(s)):
s[i] = s[i - 1] + num[i]
# 依次查询区间内的前缀和, 要对输入的l, r进行离散化操作
for i in query:
l = i[0]
r = i[1]
print(s[find(r, alls)] - s[find(l, alls) - 1]) # 映射后的小列表区间两端前缀和之差就是区间和
区间合并
区间合并首先要将所给区间进行排序,确定好最左边端点,然后遍历所给的其他区间,判断两个区间是否有交集:
是否有交集又分三种情况
- 下一个区间完全在这个区间内(则不用更新右端点)
- 下一个区间部分在这个区间内(则需要更新这个区间的右端点)
- 下一个区间完全不在这个区间内(则此区间为新的一个区间)
def merge(arr):
'''
:param arr: 输入的区间的左右端点
:return: 返回合并区间后的新区间数组
'''
res = []
arr.sort(key=lambda x: x[0], reverse=False) # 将输入的区间按照左端点升序排序
st, ed = -2e9, -2e9 # 初始化边界 防止输入的是空数组
for l, r in arr: # 遍历每个输入的区间
if ed < l: # 如果初始化的边界的右端点小于输入区间的最左边
if st != -2e9:
res.append([st, ed])
st = l # 则更新 新区间的左右端点
ed = r
else: # 否则就只更新当前区间的右端点
ed = max(ed, r)
if st != -2e9: # 最后将最后更新的区间加到总区间数组中
res.append([st, ed])
return res
if __name__ == "__main__":
n = int(input())
segs = []
for i in range(n):
l, r = map(int, input().split())
segs.append([l, r])
segs = merge(segs)
print(len(segs)) # 数组元素即是合并区间后的总数
lowbit运算资料来源:浅谈lowbit运算 - Seaway-Fu - 博客园 (cnblogs.com)
本博客题目来源:AcWing