【周记】2024暑期集训第六周

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)

查找操作的基本步骤:
  1. 递归查找
    • 如果节点 x 不是其父节点(即 x 不是根节点),则递归查找 x 的父节点。
    • 最终找到根节点并返回。
  2. 路径压缩
    • 在递归返回的过程中,将经过的每个节点的父节点直接指向根节点。这一步骤通过改变节点的父节点来“压缩路径”,从而减少树的高度。
示例

考虑一个并查集,假设我们要查找节点 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 an1modn=1
逆命题:

  • 如果 a n − 1 m o d    n = 1 a^{n-1}\mod n =1 an1modn=1 那么 n n n就是素数(不一定成立)
  • 如果 a n − 1 m o d    n ≠ 1 a^{n-1}\mod n \neq1 an1modn=1 那么 n n n就是素数(成立)
  • 随机多个a,计算 a n − 1 m o d    n a^{n-1} \mod n an1modn都等于1,n大概率也是素数
    可惜的是有很小很小很小的一部分合数,无论a取什么值他都能通过测试
    怎么排除这些合数?

二次探测定理

定理陈述:对于方程 x 2 ≡ 1 ( m o d n ) x^2 \equiv 1 \pmod{n} x21(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} x21(modn) 的解只有 x = 1 x = 1 x=1 x = n − 1 x = n-1 x=n1(模 n n n 意义下)。

随机化算法

为了检测 n n n 是否为素数,可以使用以下步骤:

  1. 选择随机整数 a a a
    随机选择一个整数 a a a,计算 a n − 1 ( m o d n ) a^{n-1} \pmod{n} an1(modn)
  2. 检查 n − 1 n-1 n1 的因子分解
    n − 1 n-1 n1 表示为 n − 1 = u ⋅ 2 t n-1 = u \cdot 2^t n1=u2t,其中 u u u 是奇数 t t t是非负整数。
  3. 计算 a n − 1 a^{n-1} an1
    根据幂运算规则,计算:
    a n − 1 = ( a u ) 2 t ( m o d n ) a^{n-1} = (a^u)^{2^t} \pmod{n} an1=(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×b1(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×b1(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} am2(modm)
    这是因为根据费马小定理 a m − 1 ≡ 1 ( m o d m ) a^{m-1} \equiv 1 \pmod{m} am11(modm),所以 a × a m − 2 ≡ 1 ( m o d m ) a \times a^{m-2} \equiv 1 \pmod{m} a×am21(modm),即 a m − 2 a^{m-2} am2 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,表示所有数最初都被认为是质数(除了 01,它们不算质数)。
    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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值