蓝桥杯备战
1. datatime模块
- 熟练掌握datatime包的一些函数,能在在处理时间模拟这一类型的题型更加快速。
- datetime.datetime
- 这个模块下的两个函数
- strftime(“%Y-%m-%d %H:%M:%S”) 转换成怎样格式的字符串
- strptime(str,格式) 这歌字符串是怎样的格式,告诉了才能知道怎么被解析
- datetime.date
- datetime.timedelta(时间)
- datetime.timestample() 可以全部转换成秒
2. 倍增思想 ST表
- RMQ问题
- ST表
-
解决可重复贡献问题
-
动态规划的思想,区间的最值问题拆解分为两个子区间的最值,状态进行转移
-
由于动态规划要求解的当前状态 依赖于已经求出的状态, 所以需要先枚举j从小到大进行枚举即可
-
st表只要懂得递推式
-
预处理出st表,然后每次询问拆解成二进制的形式
# 主要是状态转移
# i+len-1=j
# i=j-len+1 细节方面需要考虑
def st_init():
#预处理对应的二进制
l=math.ceil(math.log2(n))+1
#
dp=[[0]*l for i in range(n)]
for i in range(n):
dp[i][0]=num_list[i]
# 进行递推处理 根据动态规划具备后效性,一定要保证,更新是正确的,要调整循环次序问题,这个也是挺关键的
for j in range(1,l):
pj=1<<(j-1)
for i in range(n):
dp[i][j]=max(dp[i][j-1],dp[i+2^(j-1)][j-1])
return dp
def query(l,r):
#拆解成二进制 这个可以 通过数学证明,一定保证完全覆盖区间
#而且右边界一定在左边界 右边
s=int(math.log2(r-l+1))
#主要是细节方面
return max(dp[l][s],dp[r-2^(s)+1][s])
- 还有lca也运用了倍增的思想
- p[u][i]数组 表示节点 u往上走2^i步能走到的节点
- 倍增思想好多都是这么一个 状态定义方式
def dfs(u,fa):
deep[u]=deep[fa]+1
p[u][0]=fa
for i in range(1,21):
#从小到大进行转移
p[u][i]=p[p[u][i-1]][i-1] #
for v in G[u]:
if v==fa:
continue
dfs(v,fa)
# lca求法
# 保证x更深,做一个比较,如果不满组,则交换
# 从大到小进行枚举,如果深度不相同,则更新x节点
# 循环结束,如果二者编号相同,那么结束
# 否则开始找 lca,从大到小枚举
# 由于保证最最近共公共祖先,所以判断条件是p[x][i]!=p[y][i]
2. bisect模块
- 当对于一个单调不减(不增)的数列进行查找下标索引时,bisect模块真的非常好用
- bisect.bisect 找到大于等于的下标索引
- bisect.bisect_right 找到大于的下标索引
- bisect.insort() 查找并插入元素
- bisect.insort_letf()
3. 马拉车算法
- 最长回文子串概述
- 马拉车算法 细节处理
- 理解马拉车算法需要理解的几个概念
- dp数组:以i为中心可扩展的长度
- center,right 维护当前的最右回文子串
- 分为三种情况
- 第一种i不在回文区间中,直接暴力求解dp[i],向两端进行扩展
- 第二种情况,i在回文区间中,并且right-i>=dp[对称点],这种情况直接dp[i]=dp[对称]
- 第三种情况,i在回文区间中,但是right-i<dp[对称点],这种情况要从right,2*i-right 继续向两端扩展
- 通过发现 第二种情况和第三种情况 其实进行一个 合并,合并成为一种情况,在right-i 和 dp[对称点] 取一个最小值,然后进行扩展
- 模板代码
# 首先是向两端进行扩展
def extend(s,l,r):
while l>=0 and r<len(s) and s[l]==s[r]:
l-=1
r+=1
# 因为循环结束了,还向两端进行移动了,所以减去2
return (r-l-2)//2
def malaceh(s):
# 对s进行重新扩展
s="#"+"#".join(s)+"#"
# 维护最右回文子串
cnter,right=0,0
dp=[0]*len(s)
for i in range(1,len(s)):
#第一种情况进行判定
if i>right:
dp[i]=extend(s,i,i)
else:
#两种情况进行的一个合并
i_sys=2*center-i
mis_dis=min(dp[i_sys],right-1)
dp[i]=extend(s,i-mis_dis,i+mis_dis)
#维护最新的 右回文子串
if i+dp[i]>right:
center,right=i,i+dp[i]
return max(dp)
4. KMP算法
- kmp算法概述-next数组的含义
- next数组的求法
- 类似于递推式,next数组就类似于 逻辑上的双指针 ,还是非常容易进行一个相关的理解
- next数组的理解看作是 逻辑上的双指针,就非常容易理解
- kmp中,明白i指向s串,j才是t串即可
next=[0]*10000010
def get_next(t):
#因为next[i]表示的是0-(i-1)的最长前后缀 不包含本身,所以i从1开始
for i in range(1,len(t)):
j=next[i]
while j>0 and t[i]!=t[j]:
j=next[j]
if t[i]==t[j]:
next[i+1]=j+1
else:
next[i+1]=0
def kmp(s,t):
#返回s中t出现的次数
ans=0
j=0
for i in range(len(s)):
while j>0 and s[i]!=t[j]:
j=next[j]
if s[i]==t[j]:
j+=1
if j==len(t):
ans+=1
j=next[j]
return ans
- [契合匹配 kmp算法的应用]()
5.字典树
- 首先学习一种比较简单而且特殊的字典树,01字典树
- 01字典树 是二叉树,每个节点的权值只能是0或者1,也就是说边只能是0或者1
- 所以能够非常好的解决二进制相关问题
- 问题引入:
- 给定n个数,求两个数异或的最大值:
- 常规做法就是两重循环
- 字典树+贪心 完全就可以优化 (字典树的深度取决于树的范围,要使得异或值最大,贪心即可,找到相反的一条链即是最大值) 贪心+逐位确定
def insert(x):
global count
now=0
for j in range(31,-1,0):
pos=(x>>j)&1
if not son[now][pos]:
count+=1
son[now][pos]==count
now=son[now][pos]
# 下标 进行一个存储
val[now]=x
def query(x):
# 理解为头节点
now=0
for j in range(31,-1):
pos=(x>>j)&1
# 进行判断是否存在这个路径
if son[now][pos^1]:
#存在的话,跑到一层去
now=sonp[now][pos^1]
else:
now=son[now][pos]
# 返回 下标对应的数字
return val[now]
son=[[0]*2 for i in range(21000)]
val=[0]*21000
count=0
n=int(input())
num_list=list(map(int,input().split()))
for i in num_list:
insert(i)
- 这段代码进行一个相关的理解的话,很关键的点,就是每次创建一个节点(逻辑上的)count+1,这个count计数变量是非常的关键
- 对于son数组 第一维开多大,取决于一共要创建多少个节点这个要进行一个理解方面的
- 就是根据二维数组的值是否存在 来进行判断 当前是否存在路径
- 例题:给定一棵树和树上的边权,求两点之间路径异或值的最大值(这个是模板题目上面套了一层)
- 首先第一步,知道异或的性质 A^A=0 ,所以可以转换成 每个节点到根节点的异或值(通过dfs处理出来)转换成 两个节点的异或值
- 两个节点的异或值的最大值,显然可以通过 tire 树 进行一个相关方面的求解
#include<bits/stdc++.h>
using namespace std;
#define maxn 110000
int trie[maxn*30][2]
int val[maxn],n,head[maxn],cnt;
strcut Edge{
int nex,to,dis;
}edge[maxn<<1];
void add(int from,int to,int dis)
{
edge[++cnt].nex=head[from];
head[from]=cnt;
edge[cnt].to=to;
edge[cnt].dis=dis;
return ;
}
void dfs(int x,int fa)
{
for(int i=head[x];i;i=edge[i].nex)
{
int v=edge[i].to;
if(v==fa) continue;
x0[v]=x0[x]^edge[i].dis;
dfs(v,x)
}
return;
}
- 字典树
- 字典树两种实现方式,一种是基于类的方法,另外一种是数组模拟的方法
- 基于类的方法
class TreeNode():
def __init__(self):
#nodes表示从当前节点往下走的后续节点
self.nodes={}
#if_leaf表示是否为字符串终止标志
self.is_leaf=False
#输入字符串s # 插入的时候,都是从虚拟节点开始,所以curr=self
def insert(self,s):
#获取当前节点
curr=self
for c in s:
if c not in curr.nodes.keys():
curr.nodes[c]=TreeNode()
curr=curr.nodes[c]
#记录字符串结束标志
curr.is_leaf=True
# 判断前缀s 是否存在
def pre(self,s):
#当前节点
curr=self
for c in s:
if c not in curr.nodes.keys():
return False
curr=curr.nodes[c]
return True
- 基于二维数组 的形式 基于二维数组的方式,一定要考虑是否会超内存的问题,一般一维在1e5,就不会超内存,这一点一定要进行相关的注意
- 二维数组 一维 二维 各自都是具备各自的含义的,这一点要知道
- 第一维 一般就是 节点的个数
- 第二位就是 子节点的 含义
- 二维数组的值 就是 节点的个数
N=int(1e5)+10
son=[[0]*26 for i in range(N)]
cnt=[0]*N # 维护的是当前节点 经过了多少个字符串 节点上进行 相关维护
count=0
def insert(s,v):
global count
p=0
for c in s:
cnt[p]+=v
u=ord(c)-ord("a")
if son[p][u]==0:
count+=1
son[p][u]=count
p=son[p][u]
#不要忘记,最后这个p也要进行次数记录
cnt[p]+=v
def query(s):
p=0
ans=0
for c in s:
u=ord(c)-ord("a")
if son[p][u]==0:
# 没有路了,直接返回
return ans
p=son[p][u]
# 如果当前节点 属性值为0,表示不是前缀
if cnt[p]==0:
return ans
ans+=1
return ans
n=int(input())
ss=[]
for i in range(n):
strs=input()
insert(strs,1)
ss.append(strs)
for i in ss:
insert(i,-1)
print(query(i))
insert(i,1)
6. 单调栈单调队列
- 单调栈的应用:给定一个数组,求出数组中每个数左边或者右边第一个比它大或者小的数
-
首先需要搞清楚,需要维护的是什么,需要维护一个单调递增的单调栈还是一个单调递减的单调栈,这个需要认真分析一下过程
-
如果要求取每一个数的左边第一个比之大的数,那么应该维护一个单调递减的单调栈
-
如果要求取每一个数的左边第一个比之小的数,那么应该维护一个单调递增的单调栈
-
如果求取数组中每一个数 的右边第一个比之大的数,或者小的数,那么进行一个思维上的转换即可,将这个数组进行逆序操作,转换成 左边,减少分析过程
# 单调栈的模板题目
# 很是经典,非常经典,很是经典
# 这个输出的是编号,而不是具体的值,所以需要进行一个注意
#
N=int(input())
num_list=list(map(int,input().split()))
# 初始化一个单调栈,存储楼房的编号
# 求左边第一个比它高的楼房是哪个,所以维护一个单调递减的单调栈
# 判断的依据就是 当前元素与栈顶元素做比较,a[i]>=stack[-1]
# 遍历每一个楼房
# 存储答案 不一样
# 逻辑有点问题,还是没能够解决这些东西
def get_zuogao():
stack = []
ans=[]
for i in range(N):
while stack and num_list[i]>=num_list[stack[-1]]:
#进行出栈操作
stack.pop()
if not stack:
ans.append(-1)
else:
ans.append(stack[-1]+1)
stack.append(i)
print(*ans)
def get_yougao():
stack=[]
ans=[]
#求右边第一个比之高的数
#转换思路--将数组 逆序——然后转换成求左边 第一高的
# 还是维护一个单调递减的单调栈
num_temp=num_list[::-1]
# 因为输出的是 编号,对数组进行了一个逆序操作
# 但是 要保留原来的 编号,所以可以开一个字典进行记录
dicts={}
for i in range(N):
dicts[i]=N-i-1
#
for i in range(N):
while stack and num_temp[i]>=num_temp[stack[-1]]:
#进行出队操作
stack.pop()
if not stack:
#表明了
ans.append(-1)
else:
ans.append(dicts[stack[-1]]+1)
stack.append(i)
print(*ans[::-1])
get_zuogao()
get_yougao()
- 单调队列的应用:求给定滑动窗口的最值问题
- 可以求取一维窗口的最值问题
- 也可以求取二维 滑动窗口的最值问题
- 单调队列和单调栈 原理都是一样的,基于暴力的情况 挖掘一些性质(单调栈,队列中某一些元素永远不会作为答案输出,)然后一些相关的优化
# 单调队列,采用deque
from collections import deque
# 给定一个长度为n的数组,然后求取区间长度为k的的子区间的最大值和最小值
n,k=map(int,input().split())
num_list=list(map(int,input().split()))
q=deque()
# 遍历每一个元素
for i in range(n):
#q[0]表示队首 q[-1] 表示对尾
if q and i-k+1>q[0]:
#表明队头滑出窗口,说明,要删除q[0]
q.popleft()
# 如果要维护区间最大值的话,那么就需要维护一个 单调递减的单调队列
# 如果当前元素 与对尾元素 进行一个比较发现 num_list[i]>=q[-1] 那么就要出队
while q and num_list[i]>=num_list[q[-1]]:
q.pop()
#当前这个数的下标 进队列
q.append(i)
if i>=k-1:
print("%d"%num_list[q[0]])
- 相较于单调栈,单调队列就是多出了两点
- 一个是需要判断队头是否出队 要通过 i-k+1>q[0]做比较
- 还有一个是 什么时候输出 i-k+1>=0 的时候才能进行输出
- 这两个细节方面要进行 一个相关方面的考虑
7. 数论
7.1 同余
- 同余的概念
- 同余的性质
- 对于加减乘 这三种运算,交换和模运算的 运算顺序对 最后的结果不会产生影响的
- 如果是除法的话,那么交换运算顺序 就会对最后的结果产生影响,那么这个时候,就需要引入 乘法逆元,将除以这个数,转换成 乘以这个数的逆元,这样就不会有影响
- 求解逆元的几种方法
- 扩展的欧几里得算法 求解逆元的通用做法,不管在mod 什么意义下,都可以用这个进行一个相关的求解过程,还是非常可以的
- 费马小定理:加了约束条件,满足了这些约束条件,就可以用 快速幂快速 求解逆元
- mod的数必须是 素数
- 求乘法逆元的数 必须 与 模的数 互质 即gcd(a,n)==1
7.2 素数筛
- 埃及筛法
def get_prime(n):
vis=[0]*(n+1)
prime=[]
for i in range(2,n+1):
if vis[i]==0:
prime.append(i)
for j in range(i+i,n+1,i):
vis[j]=1
return prime
- 线性筛法
- 线性筛法 就是为了解决埃氏筛法 重复筛的过程,进一步降低了时间复杂度
- 埃是筛法 是利用当前质数的所有倍数 进行晒出掉,注意,不同质数的所有倍数当中,会有重叠的部分,所以会有重复筛的过程
- 但是线性筛法的过程,不是利用当前质数的所有倍数进行晒出,而是利用当前质数表中的所有质数的 i倍 进行晒出掉
- 一个变动的是 质数的倍数,一个变动的是质数列表中的质数
- 当然还需要进行一个特判
- 这个过程还是很简单的一个小的过程
def get_prime(n):
prime=[]
vis=[0]*int(1e5)
for i in range(2,n):
if not vis[i]:
prime.append(i)
#此时变动的不是质数的倍数,而是质数列表
j=0
while prime[j]*i<=n:
vis[prime[j]*i]=1
if i%prime[j]==0:
break
j+=1
7.3 快速幂
- 递归模板
def ksm(a,b,c):
if b==0:
return 1
if b==1:
return 1
ans=ksm(a,b//2,c)
#核心步骤:反复平方法
ans=ans*ans%c
#判断当前b基偶性
if b:
ans=ans*a%c
return ans%c
- 二进制拆分模板
def ksm(a,b,c):
ans=1
while b:
if b&1:
ans=ans*a%c
b=b>>1
a=a*a%c
return ans%c
- 矩阵快速幂 (快速幂的推广)
def mul(a,b):
m,n=len(a),len(a[0])
x,y=len(b),len(b[0])
if n!=x:
return False
c=[[0]*y for i in range(m]
for i in range(m):
for j in range(y):
for t in range(x):
c[i][j]+=a[i][t]*b[t][j]
return c
def ksm(a,b):
ans=[[1 if i==j else 0 for j in range(n)] for i in range(n)]
while b:
if b&1:
ans=mul(ans,a)
a=mul(a,a)
b=b>>1
returnb ans
- 矩阵快速幂–斐波那契数列
例题说明
# 这个属于最简单的矩阵快速幂,递推式已经给出
# 但实际情况下,递推式往往需要 自己列出,然后转换成矩阵递推式
# 建模成矩阵乘法,才能使用快速幂进行加速
mod=1000000007
def mul(a,b):
x,y=len(a),len(a[0])
m,n=len(b),len(b[0])
c=[[0]*2 for i in range(2)]
for i in range(x):
for j in range(n):
for k in range(m):
c[i][j]=(c[i][j]+a[i][k]*b[k][j])%mod
return c
def qsm(a,b):
ans=[[1,0],[0,1]]
while b:
if b&1:
ans=mul(ans,a)
a=mul(a,a)
b=b>>1
return ans
def output(a):
for x in a:
print(" ".join(map(str,x)))
# 构建矩阵
a=[[1,1],[1,0]]
b=[[1],[1]]
# 开始处理
t=int(input())
for i in range(t):
n=int(input())
if n==1 or n==2:
print(1)
else:
count=n-2
print(mul(qsm(a,count),b)[0][0])
7.4 唯一分解定理
- 唯一分解定理,也称为算术基本定理
# 优化:根号+提前停止,要进行一个判断 当已经是1的时候,提前进行一个推出
# 其实就是 就是试 除法的的修i该
def fenjie(n):
temp=int(math.sqrt(n))+1
dicts={}
for i in range(2,temp+1):
#对于每一个质数进行判断处理
count=0
flag=0
while n%i==0:
count+=1
n//=i
flag=1
# 每一轮都要进行一个判断
if flag:
dicts[i]=dicts.get(0,i)+count
if n==1:
break
if n>1:
#说明还存在一个 质数
dicts[n]=dicts.get(0,n)+1
- 唯一分解定理的性质 以及应用
- 因子个数
- 因子之和
- 快速求取 因子之和的方法
- 就是等比数列进行求和的方法,就可以快速求解得到因子之和
- 第一种方法 首先 就是想到了 质因数分解 得到各个质因数 以及对应的 幂次个数,然后运用唯一分解定理的结论,快速求和约数之和,但是时间复杂度会达到 n n n\sqrt{n} nn python 会有一个点过不了。而且需要注意的是,等比数列求和的时候,会有除法操作,也就是说,这是模数意义下的除法操作,模数意义下的除法操作,不能直接操作,否则最终的答案是错误的,这就需要引入逆元的求解
- 第一种方法的时间复杂度主要是 对每一个数(1-n)都进行了质因数分解,才得到对应的幂次,有没有一种更加快捷的方法求得 幂次,数学上是有的,比如说n=20,那么20的阶乘 会提供2 多少个幂次 通过 20/2 10/2 5/2 2/2 也就是 10+5+2+1 18个幂次,这样就能快速求解出来幂次,优化时间复杂度
- 代码如下:
# 第一种做法
def ksm(a,b,mod):
ans=1
while b:
if b&1:
ans=ans*a%mod
a=a*a%mod
b>>=1
return ans
def fenjie(x):
temp=int(math.sqrt(x))
for i in range(2,temp+1):
count=0
flag=0
while x%i==0:
count+=1
x//=i
flag=1
if flag:
dicts[i]=dicts.get(i,0)+count
# 这里一个很小的修改,没想到会带来这么大的麻烦,真的yao'jin'x
if x==1:
break
if x>1:
dicts[x]=dicts.get(x,0)+1
mod=998244353
n=int(input())
dicts={}
for i in range(2,n+1):
fenjie(i)
# 得到了 质因数和 个数 的关系,然后用等比求和公式进行求和
ans=1
for prime,times in dicts.items():
ans=ans*(ksm(prime,times+1,mod)-1)%mod*ksm(prime-1,mod-2,mod)%mod
print(ans)
# 第二种做法
def ksm(a,b,mod):
ans=1
while b:
if b&1:
ans=ans*a%mod
a=a*a%mod
b>>=1
return ans
def get_prime(n):
vis=[0]*(n+1)
prime=[]
for i in range(2,n+1):
if not vis[i]: #表明是素数
prime.append(i)
# 用素数的倍数筛
for j in range(i*i,n+1,i):
vis[j]=1
return prime
mod=998244353
n=int(input())
prime=get_prime(n)
#快速统计质约数的个数
ans=1
for p in prime:
res=0
t=n
while t:
res+=t//p
t//=p
ans=(ans*(ksm(p,res+1,mod)-1)%mod*ksm(p-1,mod-2,mod)%mod)%mod
print(ans)
7.5逆元
- 扩展的欧几里得算法 求解逆元 是基于 裴蜀定理的
- 就是通过构造解这个过程,来进行操作的
def exgcd(a,b):
#递归出口
if b==0:
return a,1,0
g,x2,y2=exgcd(b,a%b)
# 根据推出的关系式得到
x1,y1=y2,x2-(a//b)*y2
return g,x1,y1
def Func(a,b,m):
g,x1,y1=exgcd(a,b)
if m%g!=0:
return None,None,None
x0,y0=x1*m//g,y1*m//g
return g,x0,y0
def Inv(a,n):
# 为了保证得到的逆元在0-n-1 之间,所以还要加上这么一个函数
g,x,y=Func(a,n,1)
if x is None:
return None
return (x%n+n)%n
- 通过费马小定理 求解逆元,接非常快速,但是具备前提条件、
- 首先mod的数一定是 素数
- 其次,gcd(a,n)==1 也就是说 要求逆元的数·与 mod数 互质
- 这个时候a的逆元 就是 a的n-2 次方,记住这个结论
7.6 欧拉函数和欧拉降幂
-
欧拉函数定义:表示小于等于n中和n互质的数字个数
- 小于等于n
- 互质
-
欧拉函数的性质以及一些说明
- 基于这个积性函数可以推出很多性质
- 欧拉函数极性函数性质题目
在这里插入代码片
- 如何求取欧拉函数,利用唯一分解定理
- 很明显可以看出 欧拉函数的求取,只与质因数有关,与质数无关
def get_phi(n):
abs=n
m=int(n**0.5)
# 这个循环 知识枚举可能存在 的 质因数 这个方面要进行一个理解
for i in range(2,m+1):
if n%i==0:
ans=ans//i*(i-1)
while n%i==0:
n//=i
# 因为只遍历了根号,所以还有可能存在一个大于根号n的质因数,所以特判
# 如果
if n>1:
ans=ans//n*(n-1)
return ans
- 欧拉定理
- 费马小定理是欧拉定理的特殊
- 一般都是 用第三个分式,是通式,通过欧拉定理降幂之后,在采用快速幂的模板,就可以快速求解,这个满足的硬性条件就是b> ϕ ( p ) \phi(p) ϕ(p)
def ksm(a,b,c):
ans=1%c
while b:
if b&1:
ans=ans*a%c
a=a*a%c
b=b>>1
return ans%c
n,m=map(int,input().split())
mod=int(1e9)+7
# 因为是质数,所以欧拉函数
phi=mod-1
flag=0
for i in range(1,m+1):
x=x*i
if x>=phi:
# 就可以进行降幂操作
x=x%phi
flag=1
if flag:
# 根据公式得到
x+=phi
print(ksm(n,x,mod))
8. 树
- 树的存储方式:一般采用邻接表的形式
- 树的遍历 dfs遍历
- 树的遍历 并不需要判重vis 数组,图的遍历需要 判重数组vis
# 单向边
def dfs(u):
print(u)
for v in Tree[u]:
dfs(v)
dfs(root)
# 双向边
def dfs(u,fa):
print(u)
for v in Tree[u]:
if v==fa:
continue
dfs(v,u)
dfs(root,-1)
- BFS遍历层序遍历
from collections import deque
#bfs遍历模板
def bfs():
result=[]
queue=deque()
#先入队
queue.append(root)
while queue:
u=queue.popleft() #队首元素出队
result.append(u)
for v in Tree[u]:
queue.append(v)
return result
- 例题 说明
# 只有边没有说明依赖关系,没有说明父亲儿子关系,都当作无向边 进行处理
# 从根节点出发 遍历到叶子节点,每遍历到一个节点 就要把当前节点的字符添加上
# 就是一个遍历
def dfs(u,fa):
path.append(s[u])
#进行判断是否是叶子
flag=True
for v in Tree[u]:
# 进行到这个循环说明不是叶子节点
if v==fa:
continue
dfs(u,v)
flag=False
if flag:
s.add("".join(path))
#到叶子节点,就要回溯了
path.pop()
n=int(input())
s=""+input() #方便与点对应
Tree=[[] for i in range(n+1)]
for _ in range(n-1):
u,v=map(int,input().split())
Tree[u].append(v)
Tree[v].append(u)
path=[] #记录路径
s=Set()
dfs(1,0)
print(len(s))
- 树的直径
- 直径的性质
- 第二条性质 用处比较大
- 例题说明树的直径
def def(u,fa,pre=None):
global S
#维护根节点到最深叶子节点的编号
if d[u]>d[S]:
S=u
for v,w,in G[u]:
if v==fa:
continue
d[v]=d[u]+1
if pre:
#记录前驱
pre[v]=u
dfs(v,u,pre)
n=int(input())
G=[[] for i in range(n+1)]
d=[0]*(n+1)
pre=[0]*(n+1)
for _ in range(n-1):
u,v,w=map(int,input().split())
G[u].append([v,w])
G[v].append([u,w])
S=1
dfs(1,0)
d[S]=0
dfs(S,0,pre)
L=d[S]
# 获取直径所在的边
L_list=set()
while S!=0:
L_list.add(S)
S=pre[S]
for u in L_list:
for i,(v,w) in enumerate(G[u]):
if v in L_list:
G[u][i]=[v,w-1]
S=1
dfs(1,0)
- 树的重心
-
定义:最大值取得最小的节点就是 整个树的重心
-
性质
-
性质1
-
性质2
-
性质3
-
性质4
-
-
重心的求解:很明显就是求解一个mss数组,求解一个最大值中取得最小值就是重心
# 求解重心的代码逻辑很简单,就是遍历每一个节点,求解去掉这个节点之后,各个子树的最大值,下方的子树很好比较,就是dfs遍历的过程比较就好,上方的子树的话,统计一下包括当前节点下方字数之和,然后总的减去下方等于上方子树之和
def dfs(u,fa):
# 维护以当前节点为子树的节点总数量
sz[u]=1
for v in G[u]:
if v==fa:
continue
dfs(v,fa)
sz[u]+=su[v]
mss[u]=max(mss[u],su[v])
mss[u]=max(mss[u],n-sz[u])
n=int(input())
G=[[] for i in range(n+1)]
sz=[0]*(n+1)
mss=[0]*(n+1)
for _ in range(n-1):
u,v=map(int,input().split())
G[u].append(v)
G[v].append(u)
dfs(1,0)
root=mss.index(min(mss[1:]))
- LCA问题
- 定义:
-
朴素求解算法
- 祖先一定是节点编号是相同的
-
优化算法–倍增思想
- 倍增的思想,状态定义基本上都是这么进行一个定义的
- 这个p节点的一维下标表示 当前节点,二维下标表示走的2的i次方步数,二位下标的值表示 节点编号
- 通过dfs 预先处理出来,预先处理出深度数组,这个p数组处理从小到大到大即可,保证前面处理的状态已经处理出来
def lca(x,y):
# 保证x更深
if deep[x]<deep[y]:
x,y=y,x
#利用倍增思想往上走
#从大到小枚举
for i in range(20,-1,-1):
#
if deep[p[x][i]]>=deep[y]:
x=p[x][i]
#此时深度相同 此时具备两种情况,好好分析,不用记忆,现推即可
if x==y:
return x
#一起往上走 #保证是最小公共祖先
for i in range(20,-1,-1):
# 只有不等才走,才能保证是最近祖先,只有不等才可以保证是公共
if p[x][i]!=p[y][i]:
x,y=p[x][i],p[y]p[i]
return p[x][0]
- 第一步通过 dfs预处理出来 深度数组
- 第二步找到 两个点中深度更大的
- 第三步 让他跳到与另外一个点深度相同
- 第四步:判断这个点是不是lca,若不是,两个点一起跳(只有不是祖先才可以一起跳,记住这个东西)
- 树上差分
- 差分定义:
- 最直白差分的应用 就是 快速的给一段区间加上或者减去一个值
- 稍微转变一下思维,差分的应用,还可以统计树上路径某个节点出现的次数,区间某个位置查询的次数,都可以用差分进行统计,这个要进行稍微一下转换
- 树上差分
- 问题引入
- 利用dfs回溯的特性,对点的差分只需要记住 对 s,t,lca,root这四个节点进行操作就可以了,这是非常关键的
- 然后利用回溯的特性,通过一个cnt数组,相当于cnt[u]+=cnt[v]然后就可以统计出每个点染色的次数,
- 第一遍dfs先预处理出深度和p数组
- 树上差分就是 关注这四个点cnt[s]++ cnt[t]++ cnt[lca]-- cnt[root]–
- 注意这个root=p[lac][0]
- 第二遍dfs就可以进行回溯,统计出cnt数组,也就是每个点染色的次数
- 边差分
- 问题引入:
- 这个和点差分 就是多了一步将边权塞给这条边所连的深度较深的节点,然后多了一个权值
- 维护一个val数组,记录权值
- 维护一个cnt 和点差分一样,记录每个点
- 最后对于每一次询问,进行暴力处理一下就ok
- DFS序要进行学习,2023考到了省赛 异或和
-
DFS序的定义
- DFS序的求解 -
DFS序代码实现也非常简单
- 就是维护一个in 和 out的数组进行维护即可
- in[node]=++cnt
- in数组就是节点所对应的dfs序
- out数组也是这样的类似道理
-
DFS序的性质
- 在以2为根子树中,所有节点都加上一个k,那么根据DFS序的性质,我们就可以用树状数组或者线段树进行维护,根据这个dfs序
- 用线性的数据结构,去维护这个区间,单点修改,区间查询
- 线段树的下标 是 dfs序,里面的值可以根据具体情况来做修改
- 述链剖分 基于DFS序的第一个性质
- 定义:
- 重链剖分
- 这个图像就非常形象,重链剖分
9. 并查集
- 并查集注意是根节点 进行合并,这样才能保证是高效的 路径压缩就是比避免所消耗的时间复杂度过高
- 每次合并都是根节点的合并
- 直接建立并查集是比较简单的应用
- 根据图建立并查集是需要进行稍微转换一下的思路
- 需要稍微转换一下的并查集星球大战
# 删除点等价于删除该点的所有边
# 删除边求连通块和添加边求连通块是等价的操作
# 但是删除边的话,图的结构就会发生变化,所以我们从添加边入手
# 就是模拟图上的删边操作,如何进行逻辑上的删除边
# 这道题目的关键就i是 模拟图上的删除边的操作,理解了这一点,就非常容易理解下面的操作
- 可撤销并查集 (有什么任务驱动,为什么引入可撤销)
- 启发式合并操作,就是维护一个统计子树
- 可撤销并查集举例
- 分析过程:
10. 图论
- 图的dfs遍历
- dfs遍历模板
- BFS遍历
-
具体步骤
-
BFS遍历 模板题目
#标准的bfs模板
- 拓扑排序
- 定义:
- 如果想要拓扑排序是唯一的,按照字典序的顺序输出,那么就是稍微修改一下代码就好,转换成为优先队列即可,每次从队列中取出的是字典序最小的元素即可
- BFS实现拓扑排序 区别一般在于
# 拓扑排序模板题目 拓扑排序针对的是,有向无环图
# 首先找到入度为0的点,将入度为0的点都加入deque中 初始化
# 什么元素出队,队首元素出队
def top():
q=deque()
for i in range(1,n+1):
if rudu[i]==0:
q.append(i)
#初始化完成
ans=[] # 记录拓扑排序
while len(q):
#队首元素出队
u=q.popleft()
# 有的时候,出队就要判断是否到大了终止条件
# 扩展
for v in G[u]:
ru[v]-=1
# 如果度数为0的话
# 直接进行入队操作
if ru[v]==0:
q.append(v)
#终止条件判断
if len(ans)!=n:
print(-1)
else:
print(*ans,sep=" ")
- BFS 集合动态规划
- 结合动态规划的拓扑排序例题走多远
- 最短路floyed算法
- 定义:
- 初始化操作需要进行一个相关的注意
- dp[i][j]=INF
- 如果无边,等于INF,右边等于边权
- dp[i][i]=0
n,m,q=map(int,input().split())
INF=int(1e9)
#初始化为无穷
dp=[[INF]*(n+1) for i in range(n+1)]
for i in range(1,n+1):
dp[i][i]=0
for i in range(1,m+1):
u,v,w=map(int,input().split())
# 避免出现 重边,选取最小的
dp[u][v]=dp[v][u]=min(dp[u][v],w)
# floyed算法模板
for k in range(1,n+1):
for i in range(1,n+1):
for j in range(1,n+1):
# 就是以k作为中间点,看是否能进行一个更新
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
# 题目要求 不能到大救赎出-1
# 不能到大 说明 dp[i][j]=INF
for i in range(1,n+1):
for j in range(1,n+1):
if dp[i][j]==INF:
dp[i][j]=-1
for _ in range(q):
s,t=map(int,input().split())
print(dp[s][t])
# 分析题目:要想获得利润最大
# 对每一个城市都要去最大利润
# 每一个城市 可以在自己城市卖,也可以送往其他城市进行出售
# 所以可以定义每一件的利润 g_ij=sj-ai-f_ij
# 其中f_ij 表示i到j的最短距离
# 所以可以使用floyed算法 进行求解
n,m=map(int,input().split())
INF=int(1e8)
dp=[[INF]*(n+1) for i in range(n+1)]
for i in range(1,n+1):
dp[i][i]=0
for i in range(m):
u,v,w=map(int,input().split())
dp[u][v]=dp[v][u]=min(dp[u][v],w)
# 初始化完成
a=[0]*(n+1)
p=[0]*(n+1)
s=[0]*(n+1)
a[1:]=list(map(int,input().split()))
p[1:]=list(map(int,input().split()))
s[1:]=list(map(int,input().split()))
g=[[0]*(n+1) for i in range(n+1)]
# 开始进行floyed模板
for k in range(1,n+1):
for i in range(1,n+1):
for j in range(1,n+1):
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
#进行构造利润数组
for i in range(1,n+1):
for j in range(1,n+1):
g[i][j]=s[j]-s[i]-dp[i][j]
# 构造好了之后,开始求出最大利润
total_lirun=0
for i in range(1,n+1):
maxs=0
for j in range(1,n+1):
if maxs<g[i][j]:
maxs=g[i][j]
total_lirun+=maxs
print(total_lirun)
- 最短路dijstra 对比理解
- 定义:
- 和普通的bfs不同,这里的标记的时候,是出队的时候进行标记 因为第一次出队的才是距离起点最短的距离,第一次入队并不一定是
- 还有的就是 重要的操作就是 每次出队的时候 是选择 距离最短的点进行 出队操作,所以不是普通的队列,而是优先队列
- 还有一个操作就是 松弛操作
- 理解了这三点就非常容易理解dijstra算法了:处理非负边权
from queue import PriorityQueue
def dijstra(s):
#从起点s出发到各个点的最短距离
inf=int(1e8)
dp=[inf]*(n+1)
# vis[i]表示点是否出队列
vis=[0]*(n+1)
#q表示优先队列 每次出来的是距离最短的点
#所以传入的是一个二元组,距离+点
q=PriorityQueue()
dp[s]=0
q.put([dp[s],s])
while not q.empty():
#出队
dis,u=q.get()
#第一次出队的进行标记,这个和普通的dfs不同
vis[u]=1
#bfs的扩展部分 编程用u进行松弛操作
for v,w in G[u]:
if dp[v]>dp[u]+w:
dp[v]=dp[u]+w
q.put(dp[v],v)
for i in range(n+1):
if dp[i]==inf:
dp[i]=-1
print(*dp[1:],seq=" ")
#初始化起点的距离为0
dp[0]=0
q.put(0)
n,m=map(int,input().split())
G=[[] for i in range(n+1)]
for _ in range(m):
# 单相边
u,v,w=map(int,input().split())
G[u].append([v,w])
dijstra(1)
- 最短路bellman-Ford算法
- 不断利用边进行松弛
- 松弛n-1轮松弛即可求出单源最短路
- 还能用于判断负权环
# 和dijstra一样也要进行松弛操作
# 松弛操作很好理解 dp[v]=dp[u]+w
import sys
input=sys.stdin.readline
n,m=map(int,input().split())
c=[0]+list(map(int,input().split()))
# 存储图的第三中方式,直接用边存储 还有邻接表,邻接矩阵
e=[]
for _ in range(m):
u,v,w=map(int,input().split())
e.append([u,v,w])
e.append([v,u,w])
inf=int(1e10)
# 初始化
dp=[inf]*(n+1)
dp[1]=0
# 经过n-1次 循环
for i in range(n-1):
#每次循环都要遍历所有的边
for u,v,w in e:
if v!=n:
res=c[v]
else:
res=0
if dp[v]>dp[u]+w+res:
dp[v]=dp[u]+w+res
print(dp[n])
# 如果要进行判断负权环,也很好进行一个求解过程
# 直接松弛n次,如果din次还能松弛,说明存在负权换
# 通过一个标记变量进行记录
- 最小生成树
- 定义:
- 具体操作
# 最小生成树 kruskal算法
# 边排序+并查集(并查集就是合并两个集合,就是连边的操作)
n,m=map(int,input().split())
e=[]
for _ in range(m):
u,v,w=map(int,input().split())
e.append((w,u,v))
# 边排序 根据权值从小到大进行一个排序
e.sort()
# 并查集
p=list(range(n+1))
def find_root(x):
if x==p[x]:
return p[x]
p[x]=find_root(p[x])
return p[x]
def merge(a,b):
root_a=find_root(a)
root_b=find_root(b)
if root_a!=root_b:
# 进行连接的时候,是根节点之间进行连接
p[root_a]=root_b
sums,max_val=0,0
# 从小到大枚举所有边,进行合并
for w,u,v in e:
root_u=find_root(u)
root_v=find_root(v)
if root_u!=root_v:
p[root_u]=root_v
sums+=1
max_val=max(max_val,w)
print(sums,max_val)
- Prim算法
-
距离集合最小的点
-
算法步骤:
# prim 算法是不断加点的过程,
# kru 算法是 不断加边的过程
# prim 算法又和 dijstra算法 有点不一样
# prim算法 每次找到离集合最近的点,dijstra算法每次找到 离起点最近的点
inf=int(1e9)
n,m=map(int,input().split())
maps=[[inf]*(n+1) for i in range(n+1)]
for _ in range(m):
u,v,w=map(int,input().split())
maps[u][v]=maps[v][u]=min(maps[u][v],w)
# 这歌和floyed算法 有一些区别,floyed算法初始化还要 dp[i][i]=0
d=[inf]*(n+1)
max_val=0
# 初始化离集合最近的点是从 1号点开始
u=1
# 进行n-1次 循环
for i in range(n-1):
# 将这个离集合最近的点加入集合中
d[u]=0
next_u,next_val=0,inf
#利用这个离集合最近的点去更新其他点到集合的距离 并且找到下一个离集合最近的点
for v in range(1,n+1):
if d[v]==0:
continue # 不能选择已经在集合中的点
d[v]=min(d[v],map[u][v])
#找到 离集合更近的点
if d[v]<next_val:
next_val=d[v]
next_u=v
u=next_u
max_val=max(max_val,next_val)
print(n-1,max_val)
11.树状数组
- 前置知识lowbit操作
- tree[i] 树状数组的含义:
- tree[x]所覆盖的区间长度就是lowbit(x)
- tree[x]节点的父亲节点为tree[x+lowbit(x)]
- 明白树状数组的定义方式
- 求取前缀和 就是不断的进行区间拆分操作,将一个大的区间拆分成为小的区间之和,通过x-lowbit(x)核心操作实现
- 区间查询+单点修改
def lowbit(x):
return x&(-x)
# 区间查询,单点修改
# 进行区间查询非常简单,就是将一个大区间拆解成为若干个小的区间
# 通过不断x-lowbit(x)实现
def query(x,tree):
ans=0
#求前缀和,进行区间拆分
while x:
ans+=tree[x]
x-=lowbit(x)
return ans
# 进行单点修改的话,
## 首先要知道哪些区间包含了 这个单点,对单点进行修改,对应的区间也会发生变化
# 这个单点进行修改,需要找到哪些区间包含这些单点
# 如果修改为y 那么tree+=(y-tree[x]) 必须是这种增量形式
def add(x,y,tree,n):
while x<=n:
tree[x]+=y
# 找到父亲节点,父亲节点必然包含这个单点
x+=lowbit(x)
n=int(input())
a=[0]+list(map(int,input().split()))
tree=[0]*(n+1)
# 初始化树状数组
for i in range(1,n+1):
#这个初始化方式和差分数组的初始化方式有点类似
add(i,a[i],tree,n)
# m次操作
m=int(input())
for i in range(m):
op,a,b=map(int,input().split())
if op==1:
add(a,b,tree,n)
else:
- 区间修改+单点查询
- 进行一个转换,又可以变成区间查询+单点修改
# 区间修改,单点查询
# 整体流程,可以转换成为 区间查询+单点修改
def lowbit(x):
return x&(-x)
def query(x,tree):
ans=0
while x:
#拆分区间的操作
ans+=tree[x]
x-=lowbit(x)
return ans
def add(x,y,tree,n):
# 单点修改
while x:
tree[x]+=y
x+=lowbit(x)
n=int(input())
a=[0]+list(map(int,input().split()))
tree=[0]*(n+1)
for i in range(1,n+1):
add(i,a[i],tree,n)
chafen=[0]*(n+1)
for i in range(1,n+1):
chafen[i]=a[i]-a[i-1]
# 构造完差分数组,之后
# 其余操作完全一样,区间修改,转换成两点修改
# 单点查询 转换成为 前缀和操作
# 看到情况1:发现是单点修改
# 看到情况2:发现经过对题目的式子进行一个转换 是一个区间查询
# 那么就是标准的树状数组
def lowbit(x):
return x&(-x)
def query(x):
ans=0
while x:
ans+=tree[x]
x-=lowbit(x)
return ans
def add(x,y):
while x<=n:
tree[x]+=y
x+=lowbit(x)
n,m=map(int,input().split())
a=[0]+list(map(int,input().split()))
tree=[0]*(n+1)
for i in range(1,n+1):
add(i,a[i])
for i in range(m):
op=list(map(int,input().split()))
if op[0]==1:
x,z=op[1],op[2]
add(x,z-a[x])
a[x]=z
else:
i=op[1]
ans=(2*i-n-2)*a[i]+query(n)-2*query(i-1)
print(ans)
- 总结 前缀区间维护最大值,异或和都可以,但是对于复杂的区间问题,树状数组就有点难搞了,这个时候引入线段树
- 二维树状数组
# 单点修改
def add(x,y):
while x<=n:
tree[x]+=y
x+=lowbit(x)
def add(x,y,z):
i=x
while i<=n:
j=y
while j<+m:
tree[i][j]+=z
j+=lowbit(j)
i+=lowbit(i)
# 对比区间查询
def query(x):
ans=0
while x:
ans+=tree[x]
x-=lowbit(x)
return ans
# 求取矩阵 (1,1)- (x,y)之和
def query(x,y):
ans=0
i=x
while i>0:
j=y
while j>0:
ans+=tree[i][j]
j-=lowbit(j)
i-=lowbit(i)
return ans
- 一维求取区间和二维求子矩阵和 (前缀和是离线的操作,树状数组是动态的操作)
- 树状数组的应用:不仅仅进行前缀和这个简单的操作
def descrete(a):
b=list(set(a))
b.sort()
ans=[]
for i in range(len(a)):
ans.append(bisect_left(b,a[i])+1) # 离散化后从1开始
return ans
def lowbit(x):
return x&(-x)
def add(x,y):
while x<=n:
tree[x]+=y
x+=lowbit(x)
def query(x):
res=0
while x:
res+=tree[x]
x-=lowbit(x)
return res
n=int(input())
a=list(map(int,input().split()))
a=descrete((a))
a=[0]+a
ans=0
tree=[0]*(n+1)
for j in range(1,n+1):
#将a[j]放入树状数组,此处的a[j]下标
add(a[j],1)
ans+=j-query(a[j])
print(ans)
12. 离散化操作
- 离散化:不关注数字本身,只关注大小关系时,利用排名替代原始数据
- 数组离散化的步骤:
- 把a拷贝一份设置为b
- 对b排序,去重
- 将a中每个元素设置为b数组中的下标(二分查找)
- 一般不会单独考察离散化操作,一般会结合树状数组,线段树等结构一起考察
- 代码如下:
from bisect import bisect_left
def discrete(a):
b=list(set(a))
b.sort()
ans=[]
for x in a:
ans.append(bisect_left(b,x))
return ans
#第二种方法,通过一个字典进行相关操作
# 两种方法都可以 进行一个离散映射过程
values=list(range(len(b)))
dicts=dict(zip(b,values))
ans=[]
for x in a:
ans.append(dicts[x])
return ans
13. 构造
- 构造问题常见的题型
- 数学问题:
- 图论问题:这个算是比较有难度的构造方法
- 字符串处理:这个需要发现规律,并且需要进行一个相关方面的一个总结过程
- 构造的应用题型
- 经典例题解析
- 解析如下:真数学题
- 经典例题解析:
- 通过这个 案例初步分析 2 5 7 公差满足的性质 一定小于等于 最小的间隔,从最小的间隔开始遍历,看是否满足条件(就是写一个check函数,看从最小值出发,生成的序列,一定要包含这N个数,);如果满足条件,那么就可以直接跳出循环,因为询问的是 最小项数
- 正解这个答案,真的感觉并不是那么容易进行一个想得出来的,还是非常难,转换成 gcd构造,真的不容易思考出来
- 经典例题分析:图论上的构造问题
- 通过分析题目,可以初步得到
- 经典例题解析
- 经典例题解析