AtCoder Beginner Contest 368 记录
A - Cut
总结
把数组后k个元素移动到数组前面并输出。直接切片就好了
代码实现
n,k=map(int, input().split())
a=list(map(int, input().split()))
print(*a[-k:]+a[:-k])
B - Decrease 2 max elements
总结
按照题目模拟就好了,写一个函数用来判定,满足题意的时候退出模拟。
代码实现
def count_positive(a):
count=0
for i in a:
if i>0:
count+=1
if count>1:
return False
else:
return True
n=int(input())
a=list(map(int, input().split()))
ans=0
while not count_positive(a):
a.sort()
a[-1]-=1
a[-2]-=1
ans+=1
print(ans)
C - Triple Attack
总结
这题卡了我很久,t
初始为0,每次操作t+1
,如果t
不是3的倍数就给数组第一个大于0的数减1,否则减3,直到所有元素都不大于0。数据很大直接模拟的话超时。于是就考虑把三次操作看成一组:-1,-1,-3(即一次性给这个元素-5,然后t+3)。从第一个元素开始操作直到第一个元素不大于0然后往后对下一个操作,直到处理完毕。但是还要考虑一些情况,如果那个元素不是5的倍数,按照三次一组的操作结束后还得再操作几次直到符合,这题最难想的点也就在这,因为做完额外的操作之后还会影响到之后的元素,所以我用一个变量cy
来标记目前的操作是周期(1,1,3)中的第几个。然后再三次一组的操作结束后直接判断是不是还需要操作,若需要的话将这个元素操作一次(-1)然后更新cy
为1。接着判断这个数是否处理完毕,如果还需要处理那么就再-1,再把cy
更新成2。如果还要处理,那就再-3,然后cy
更新成0。这便是额外处理的所有情况。一个数处理完毕后cy
会继承到后一个数,如果cy
不为0,说明上一次操作结束后还做了额外的操作,所以要先把目前这一轮周期给走完做一个预处理。
越说越乱了,确实有点难想,直接看代码。
n = int(input())
h = list(map(int, input().split()))
t = 0
cy = 0
for i in range(n):
while h[i] > 0:
if cy == 0:
if h[i] >= 5:
full_cycles = h[i] // 5
t += full_cycles * 3
h[i] -= full_cycles * 5
else:
if h[i] >= 1:
h[i] -= 1
t += 1
cy = 1
elif cy == 1:
h[i] -= 1
t += 1
cy = 2
elif cy == 2:
h[i] -= 3
t += 1
cy = 0
print(t)
这个方法看着就很乱,所以经过我的思考,找到了一种更简洁的写法。
对每一个元素进行操作直到小于等于0就对下一个操作。每次操作对先尝试当前元素整除5,也就是上面所讲的三个一组来操作,将这个结果乘3加给t
就好了,然后再算一下操作完之后当前元素还剩下多少,这时候就不该考虑那么多,直接变成一开始的暴力模拟来把剩下的搞定。也不用考虑新加一个变量来记录在周期中的位置,直接看t
的值就可以了。
代码实现
n = int(input())
H = list(map(int, input().split()))
t = 0
for h in H:
t+=h//5*3
h%=5
while h>0:
t+=1
if t%3==0:
h-=3
else:
h-=1
print(t)
算法学习记录
并查集
- 在并查集(Union-Find)结构中,每个集合通过一个“父节点”来表示。初始时,每个节点是自己的父节点,随着合并操作的进行,某些节点的父节点会被更新,以表示它们属于同一集合。
通过这种方式,可以快速地判断两个节点是否在同一集合,以及将它们合并到同一个集合中。
路径压缩
路径压缩的原理
在执行查找操作时,路径压缩会将树中经过的所有节点直接连接到根节点上。这样做可以显著减少树的深度,提升后续操作的效率。时间复杂度近乎O(1)
查找操作的基本步骤:
- 递归查找:
- 如果节点
x
不是其父节点(即x
不是根节点),则递归查找x
的父节点。 - 最终找到根节点并返回。
- 如果节点
- 路径压缩:
- 在递归返回的过程中,将经过的每个节点的父节点直接指向根节点。这一步骤通过改变节点的父节点来“压缩路径”,从而减少树的高度。
示例
考虑一个并查集,假设我们要查找节点 x
的根节点,经过路径压缩的查找操作如下:
def find_parent(x):
if x != father[x]:
# 递归查找父节点,并在递归返回时进行路径压缩
father[x] = find_parent(father[x])
return father[x]
在这个函数中:
father[x]
是x
的父节点。如果x
不是根节点(x != father[x]
),则递归调用find_parent
来找到根节点。- 在递归返回的过程中,将
x
的父节点直接设置为根节点,完成路径压缩。
亲戚 洛谷P1551
代码实现
C++
#include<bits/stdc++.h>
using namespace std;
int n,m,q,f[10010],c,d,a,b;
int fd(int x)//找出x家的大佬 也就是二叉树的祖先节点
{
if(f[x]==x)//x是x的爸爸,简单的来说就是x没爸爸了
//他是家里最大的大佬,所以返回的x就是我们所求的祖先节点
return x;
else
return f[x]=fd(f[x]);//x不是他自己的爸爸,所以他上面还
//有爸爸,我们的目标是祖先节点,所以我们此时要做的是问他
//爸爸的爸爸是谁,即再使用一次fd(find)函数【其实就是一个递归过程
}
void hb(int x,int y)
{
f[fd(y)]=fd(x);//合并x子集和y子集,直接把x子集的祖先节
//点与y子集的祖先节点连接起来,通俗点来说就是把x的最大祖
//先变成y子集最大祖先的爸爸
return ;
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++)
f[i]=i;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&c,&d);
hb(c,d);
}
for(int i=1;i<=q;i++)
{
scanf("%d%d",&a,&b);
if(fd(a)==fd(b))//如果a所在子集的大佬[前面已经解释过了]和b所在子集的大佬一样,即可知a和b在同一个集合
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
python
# 查找元素x的父节点,使用路径压缩优化
def find_parent(x):
# 如果x不是自己的父节点,则递归查找x的父节点,并进行路径压缩
if x != father[x]:
father[x] = find_parent(father[x])
# 返回x的根节点
return father[x]
# 合并两个集合,u和v属于同一集合
def merge(x, y):
father[find_parent(x)]=find_parent(y)
#合并x子集和y子集,直接把x子集的祖先节点与y子集的祖先节点连接起来,通 俗点来说就是把x的最大祖先变成y子集最大祖先的爸爸
# 从输入中读取n(节点数)、m(合并操作数)和p(查询对数)
n, m, p = map(int, input().split())
# 初始化每个节点的父节点为自身
father = [i for i in range(n + 1)]
# 读取m个合并操作
for _ in range(m):
mi, mj = map(int, input().split())
# 合并mi和mj所在的集合
merge(mi, mj)
# 读取p个查询操作
for _ in range(p):
pi, pj = map(int, input().split())
# 查询pi和pj是否属于同一集合
if find_parent(pi) == find_parent(pj):
print("Yes") # 如果属于同一集合,输出"Yes"
else:
print("No") # 如果不属于同一集合,输出"No"
按大小合并
size
:记录每个根节点代表的集合的大小。初始时,每个集合的大小是1,因为每个节点开始时都是独立的集合。
合并操作负责将两个集合合并。在按大小合并中,我们总是将较小的集合合并到较大的集合中。这样可以保持树的高度较低,从而提高效率。
def merge(u, v):
pu = find_parent(u)
pv = find_parent(v)
if pu != pv:
# 按大小合并:将较小的集合合并到较大的集合中
if size_arr[pu] < size_arr[pv]:
parent[pu] = pv
size_arr[pv] += size_arr[pu]
else:
parent[pv] = pu
size_arr[pu] += size_arr[pv]
素数
关于素数的小知识
1.素数数量无穷多
2.随着整数增大,素数分布越稀疏
3.随机整数x,x是素数的概率是
1
l
n
2
x
\frac{1}{ln_2x}
ln2x1
4.1~n大约有
n
l
n
(
n
)
\frac{n}{ln(n)}
ln(n)n
5.唯一分解定理,每个数都可以唯一分解成质数的乘积
猜想
哥德巴赫猜想
每一个大于2的偶数都可表示为两个素数之和。
另一种等价表述是:
每一个大于等于6的偶数都可表示为两个连续素数之和。
可以推出:
每一个不小于6的正整数n,可以分成三个素数之和
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
def goldbach(n):
for i in range(2, n):
if is_prime(i):
for j in range(i, n - i):
if is_prime(j) and is_prime(n - i - j):
return i, j, n - i - j
波特兰猜想
对任意给定的n>1,存在p,p是素数,且n<p<2n
孪生素数猜想
有无穷多个形如p,p+2的素数对
费马小定理
费马小定理:如果
n
n
n是素数,那么
a
n
−
1
m
o
d
n
=
1
a^{n-1}\mod n =1
an−1modn=1
逆命题:
- 如果 a n − 1 m o d n = 1 a^{n-1}\mod n =1 an−1modn=1 那么 n n n就是素数(不一定成立)
- 如果 a n − 1 m o d n ≠ 1 a^{n-1}\mod n \neq1 an−1modn=1 那么 n n n就是素数(成立)
- 随机多个a,计算
a
n
−
1
m
o
d
n
a^{n-1} \mod n
an−1modn都等于1,n大概率也是素数
可惜的是有很小很小很小的一部分合数,无论a取什么值他都能通过测试
怎么排除这些合数?
二次探测定理
定理陈述:对于方程
x
2
≡
1
(
m
o
d
n
)
x^2 \equiv 1 \pmod{n}
x2≡1(modn),如果
x
x
x 有除了
x
=
1
x = 1
x=1 和 $x = n-1 $之外的解,那么
n
n
n不是素数。
推论:如果
n
n
n 是素数,则方程
x
2
≡
1
(
m
o
d
n
)
x^2 \equiv 1 \pmod{n}
x2≡1(modn) 的解只有
x
=
1
x = 1
x=1 和
x
=
n
−
1
x = n-1
x=n−1(模
n
n
n 意义下)。
随机化算法
为了检测 n n n 是否为素数,可以使用以下步骤:
- 选择随机整数
a
a
a:
随机选择一个整数 a a a,计算 a n − 1 ( m o d n ) a^{n-1} \pmod{n} an−1(modn)。 - 检查
n
−
1
n-1
n−1 的因子分解:
将 n − 1 n-1 n−1 表示为 n − 1 = u ⋅ 2 t n-1 = u \cdot 2^t n−1=u⋅2t,其中 u u u 是奇数 t t t是非负整数。 - 计算
a
n
−
1
a^{n-1}
an−1:
根据幂运算规则,计算:
a n − 1 = ( a u ) 2 t ( m o d n ) a^{n-1} = (a^u)^{2^t} \pmod{n} an−1=(au)2t(modn)
代码模版
import random
def mod_exp(base, exp, mod):
"""计算 base^exp % mod 的值"""
result = 1
base = base % mod
while exp > 0:
if (exp % 2) == 1: # 如果 exp 是奇数
result = (result * base) % mod
exp = exp >> 1 # 将 exp 右移一位
base = (base * base) % mod
return result
def millerRabin(n, test_time=8):
"""
使用米勒-拉宾测试判断 n 是否为素数
:param n: 要测试的整数
:param test_time: 测试次数,建议设为不小于 8 的整数以保证正确率,但也不宜过大,影响效率
:return: 如果 n 是素数,返回 True;否则返回 False
"""
if n < 3 or n % 2 == 0:
return n == 2
if n % 3 == 0:
return n == 3
# 将 n-1 分解为 2^t * u,其中 u 为奇数
u, t = n - 1, 0
while u % 2 == 0:
u = u // 2
t = t + 1
# 进行 test_time 次测试
for _ in range(test_time):
# 0, 1, n-1 可以直接通过测试, a 取值范围 [2, n-2]
a = random.randint(2, n - 2)
v = mod_exp(a, u, n)
if v == 1:
continue
s = 0
while s < t:
if v == n - 1:
break
v = (v * v) % n
s = s + 1
# 如果未找到非平凡平方根,则说明 n 不是素数
if s == t:
return False
return True
# 示例测试
n =int(input()) # 你可以更改为其他整数进行测试
print(f"{n} 是素数" if millerRabin(n) else f"{n} 不是素数")
逆元
逆元 是数学中一种重要的概念,特别是在数论和代数结构中。它主要涉及到对乘法运算的逆操作。下面是逆元的一些基本定义和性质:
1. 逆元的定义
模运算下的逆元:
在模
m
m
m 的意义下,给定一个整数
a
a
a 和一个正整数
m
m
m,如果存在一个整数
b
b
b,使得
a
×
b
≡
1
(
m
o
d
m
)
a \times b \equiv 1 \pmod{m}
a×b≡1(modm),那么
b
b
b 就被称为
a
a
a 在模
m
m
m 下的逆元。这个关系可以表示为:
a × b ≡ 1 ( m o d m ) a \times b \equiv 1 \pmod{m} a×b≡1(modm)
这个 b b b 就是 a a a 的模 m m m 的逆元。
2. 存在性条件
一个整数 a a a 在模 m m m 下有逆元,当且仅当 a a a 和 m m m 互质,即它们的最大公约数 gcd ( a , m ) \gcd(a, m) gcd(a,m) 为 1。换句话说,只有当 a a a 和 m m m 互质时, a a a 才有逆元。
3. 计算方法
-
扩展欧几里得算法:可以用扩展欧几里得算法来计算模 m m m 下的逆元。扩展欧几里得算法不仅可以求得 a a a 和 m m m 的最大公约数,还可以找出使得 a × x + m × y = gcd ( a , m ) a \times x + m \times y = \gcd(a, m) a×x+m×y=gcd(a,m) 成立的 x x x 和 y y y。当 gcd ( a , m ) = 1 \gcd(a, m) = 1 gcd(a,m)=1 时, x x x 就是 a a a 的逆元。
-
费马小定理:如果 m m m 是一个质数,则对于任意的整数 a a a(且 a a a 不被 m m m 整除), a a a 在模 m m m 下的逆元可以通过以下公式计算:
a m − 2 ( m o d m ) a^{m-2} \pmod{m} am−2(modm)
这是因为根据费马小定理 a m − 1 ≡ 1 ( m o d m ) a^{m-1} \equiv 1 \pmod{m} am−1≡1(modm),所以 a × a m − 2 ≡ 1 ( m o d m ) a \times a^{m-2} \equiv 1 \pmod{m} a×am−2≡1(modm),即 a m − 2 a^{m-2} am−2 是 a a a 的逆元。
线性筛素数(模版)
线性筛法通过利用已有素数来筛选新的合数,从而高效地找出所有素数。
#include <bits/stdc++.h>
bool isPrime[100000010];
//isPrime[i] == 1表示:i是素数
int Prime[6000010], cnt = 0;
//Prime存质数
void GetPrime(int n)//筛到n
{
memset(isPrime, 1, sizeof(isPrime));
//以“每个数都是素数”为初始状态,逐个删去
isPrime[1] = 0;//1不是素数
for(int i = 2; i <= n; i++)
{
if(isPrime[i])//没筛掉
Prime[++cnt] = i; //i成为下一个素数
for(int j = 1; j <= cnt && i*Prime[j] <= n/*不超上限*/; j++)
{
//从Prime[1],即最小质数2开始,逐个枚举已知的质数,并期望Prime[j]是(i*Prime[j])的最小质因数
//当然,i肯定比Prime[j]大,因为Prime[j]是在i之前得出的
isPrime[i*Prime[j]] = 0;
if(i % Prime[j] == 0)//i中也含有Prime[j]这个因子
break; //重要步骤。见原理
}
}
}
int main()
{
int n, q;
scanf("%d %d", &n, &q);
GetPrime(n);
while (q--)
{
int k;
scanf("%d", &k);
printf("%d\n", Prime[k]);
}
return 0;
}
import sys
import numpy as np
input = sys.stdin.read
def get_primes(n):
is_prime = np.ones(n + 1, dtype=bool)
is_prime[:2] = False
p = 2
while (p * p <= n):
if is_prime[p]:
is_prime[p*p:n+1:p] = False
p += 1
primes = np.nonzero(is_prime)[0]
return primes
data = input().split()
n = int(data[0])
q = int(data[1])
primes = get_primes(n)
query_indices = map(int, data[2:])
results = [primes[k-1] for k in query_indices]
print('\n'.join(map(str, results)))
dijkstra算法
Dijkstra算法是一种用于计算图中最短路径的贪心算法,特别适用于具有非负权重的图。它由荷兰计算机科学家艾兹赫尔·迪赫斯特(Edsger Dijkstra)于1956年提出,并在1959年发表。
原理
Dijkstra算法通过逐步找到距离起点最近的节点来找到最短路径。其基本步骤如下:
初始化:
创建一个最小优先队列,用于存储待处理节点及其当前最短距离。
将起始节点的距离设为0,其他所有节点的距离设为无限大。
将起始节点添加到队列中。
处理节点:
从队列中提取距离最小的节点,标记为已访问。
更新该节点的所有相邻节点的最短距离。如果通过当前节点访问相邻节点的距离比之前记录的距离短,则更新相邻节点的距离,并将相邻节点添加到队列中。
重复:
重复第2步,直到队列为空或找到目标节点的最短路径。
Python模版P4779
import heapq
def dijkstra(adj, n, s):
dis = [float('inf')] * (n + 1) # 距离数组, 初始化为无穷大
tag = [False] * (n + 1) # 标记数组, 用于判断是否已经处理过
dis[s] = 0 # 起点到起点的距离为0
Q = [(0, s)] # 优先队列, 元素为(距离, 节点)
while Q:
while Q and tag[Q[0][1]]:
heapq.heappop(Q)
if not Q:
break
dist, x = heapq.heappop(Q) # 取出距离最小的节点
dis[x] = dist
tag[x] = True
for y, w in adj[x]:
if not tag[y]:
heapq.heappush(Q, (dis[x] + w, y))
return dis
n,m,s=map(int, input().split())
adj = [[] for _ in range(n + 1)] # 邻接表
for _ in range(m):
x, y, z = map(int, input().split())
adj[x].append((y, z))
dis = dijkstra(adj, n, s)
print(*dis[1:])
埃拉托斯特尼筛法
埃拉托斯特尼筛法(Sieve of Eratosthenes)是一种高效的算法,用于找出所有小于或等于给定正整数 n 的质数。
1.初始化:
is_p
数组的每个元素初始化为True
,表示所有数最初都被认为是质数(除了0
和1
,它们不算质数)。
2.标记过程:- 从
2
开始遍历所有数字,如果is_p[i]
为True
,表示 i 是一个质数。
接着,所有i
的倍数(从i*2
开始)都被标记为非质数(is_p[j] = False
)。这是因为一个质数的倍数不可能是质数。
3.质数判断: - 当算法完成后,
is_p[i]
为True
的所有i
值就是质数,而False
的则不是质数。
例题
求出1-n之间所有质因数个数大于1的数的个数。真题
思路
通过埃氏筛,可以把1-n之间所有质数确定,在这个过程中,每发现一个质数,就会把这个质数的倍数确定为合数,而被确定的这个合数也是在1-n的范围内,这就意味着他可以分解出当前这个质数,所以用一个数组ct在筛选质数的同时记录下1-n之间每一个数可以分解出的质因数个数。由于这里最终统计的时候不用考虑0和1,所以埃氏筛的初始化可以省略将0和1设置为False的步骤。
代码实现
n=int(input())
ct=[0]*(n+1)
is_p=[True]*(n+1)
for i in range(2,n+1):
if is_p[i]:
for j in range(i*2,n+1,i):
is_p[j]=False
ct[j]+=1
ans = sum(1 for c in ct if c > 1)
print(ans)