板子集合
单调队列
#include<bits/stdc++.h>
using namespace std;
int a[2000005]; // 定义数组存储输入数据
int main(){
int n,k;cin>>n>>k; // 输入n个元素和窗口大小k
for(int i=1;i<=n;++i){
cin>>a[i]; // 读入数组元素
}
deque<int>q1; // 双端队列,用于求滑动窗口最小值
for(int i=1;i<=n;++i){
// 维护队列单调递增:删除队尾比当前元素大的元素
while(!q1.empty()&&a[q1.back()]>=a[i])q1.pop_back();
q1.push_back(i); // 当前元素下标入队
// 删除不在当前窗口内的元素(下标小于等于i-k)
while(!q1.empty()&&q1.front()<=i-k)q1.pop_front();
if(i>=k){
cout<<a[q1.front()]<<' '; // 输出当前窗口最小值
}
}
cout<<'\n';
deque<int>q; // 双端队列,用于求滑动窗口最大值
for(int i=1;i<=n;++i){
// 维护队列单调递减:删除队尾比当前元素小的元素
while(!q.empty()&&a[q.back()]<=a[i])q.pop_back();
q.push_back(i); // 当前元素下标入队
// 删除不在当前窗口内的元素(下标小于等于i-k)
while(!q.empty()&&q.front()<=i-k)q.pop_front();
if(i>=k){
cout<<a[q.front()]<<' '; // 输出当前窗口最大值
}
}
}
单调栈
#include<bits/stdc++.h>
using namespace std;
int a[3000006]; // 定义全局数组a,用于存储输入的n个整数
// 快速读入函数(用于优化大量数据输入)
inline int read(){
int x = 0, f = 1; // x存储结果,f存储正负号
char ch = getchar_unlocked(); // 使用非锁定版本获取字符,速度更快
while (!isdigit(ch)){ // 跳过非数字字符
if (ch == '-')
f = -1; // 遇到负号标记为负数
ch = getchar_unlocked();
}
while (isdigit(ch)){ // 处理数字部分
x = (x << 1) + (x << 3) + (ch ^ 48); // 等价于x*10 + (ch-'0'),使用位运算优化
ch = getchar_unlocked();
}
return x * f; // 返回结果乘以符号
}
// 快速输出函数(用于优化大量数据输出)
inline void write(int x){
if (x < 0) putchar('-'), x = -x; // 处理负数
if (x > 9) write(x / 10); // 递归输出高位数字
putchar(x % 10 + '0'); // 输出当前位数字
}
// 定义结构体f,存储元素下标和值
struct f{
int num; // 元素下标
int d; // 元素值
};
int main(){
stack<f>s; // 主栈,用于存储结构体f(元素下标和值)
stack<int>s2; // 辅助栈,用于临时存储结果,最后逆序输出
int n;n=read(); // 使用快速读入读取n
for (int i=1;i<=n;++i)a[i]=read(); // 使用快速读入读取数组a
// 从右向左遍历数组
for (int i=n;i>=1;--i){
// 维护单调栈:弹出栈顶所有值小于等于当前元素的元素
while (!s.empty()&&s.top().d<=a[i])s.pop();
// 如果栈不为空,说明找到了右边第一个大于a[i]的元素
if (!s.empty())s2.push((s.top().num)); // 将找到的元素下标存入结果栈
else s2.push(0); // 栈为空说明右边没有更大的元素,存入0
// 将当前元素压入栈中
s.push({i,a[i]});
}
// 输出结果:由于是逆序存储,需要弹出栈中所有元素
while (!s2.empty()){
write(s2.top()); // 使用快速输出
putchar(' '); // 输出空格
s2.pop(); // 弹出已输出元素
}
return 0;
}
并查集
#include<bits/stdc++.h>
using namespace std;
int f[200005]; // 并查集数组,f[i]表示i的父节点
// 查找x的根节点,带路径压缩优化
int find(int x){
if (f[x]!=x)f[x]=find(f[x]); // 如果x不是根,递归查找并压缩路径
return f[x]; // 返回根节点
}
// 合并x和y所在的集合
void join(int x,int y){
int f1=find(x),f2=find(y); // 找到x和y的根节点
if (f1!=f2)f[f1]=f2; // 如果不在同一集合,将f1的根指向f2
}
int main(){
int n,m;
cin>>n>>m; // 输入元素个数n和操作次数m
for (int i=1;i<=n;++i){f[i]=i;} // 初始化并查集,每个元素的父节点是自己
while (m--){
int z;cin>>z; // 输入操作类型
if (z==1){ // 操作1:合并操作
int x,y;cin>>x>>y;
join(x,y); // 合并x和y所在的集合
}
else{ // 操作2:查询操作
int x,y;cin>>x>>y;
if (find(x)!=find(y))cout<<"N"<<'\n'; // 不在同一集合输出N
else cout<<"Y"<<'\n'; // 在同一集合输出Y
}
}
}
普通线段树
**#include<bits/stdc++.h>
using namespace std;
int w[500005*4],a[500005],lzj[500005*4],lzc[500005*4],p; // w:线段树节点值 a:原始数组 lzj:加法懒标记 lzc:乘法懒标记 p:模数
// 上传节点信息
void pushup(int u){
w[u]=(w[u*2]+w[u*2+1])%p;
}
// 构建线段树
void build(int u,int l,int r){
lzc[u]=1; // 乘法懒标记初始化为1
lzj[u]=0; // 加法懒标记初始化为0
if(l==r){
w[u]=a[l]%p;
return;
}
int m=(l+r)/2;
build(u*2,l,m);
build(u*2+1,m+1,r);
pushup(u);
}
// 判断[l,r]是否完全在[L,R]内
bool in(int l,int r,int L,int R){
return (L<=l)&&(r<=R);
}
// 判断[l,r]是否完全在[L,R]外
bool of(int l,int r,int L,int R){
return (r<L)||(R<l);
}
// 下传懒标记
void pushdown(int u,int l,int r){
int m=(l+r)/2;
// 更新左儿子:先乘后加
w[u*2]=(1LL*w[u*2]*lzc[u]%p+1LL*lzj[u]*(m-l+1)%p)%p;
lzc[u*2]=1LL*lzc[u*2]*lzc[u]%p;
lzj[u*2]=(1LL*lzj[u*2]*lzc[u]%p+lzj[u])%p;
// 更新右儿子:先乘后加
w[u*2+1]=(1LL*w[u*2+1]*lzc[u]%p+1LL*lzj[u]*(r-m)%p)%p;
lzc[u*2+1]=1LL*lzc[u*2+1]*lzc[u]%p;
lzj[u*2+1]=(1LL*lzj[u*2+1]*lzc[u]%p+lzj[u])%p;
// 重置当前节点懒标记
lzc[u]=1;
lzj[u]=0;
}
// 单点修改
void uqd1(int u,int l,int r,int x,int p){
if(l==r){
w[u]=x%p;
return;
}
pushdown(u,l,r);
int m=(l+r)/2;
if(p<=m)uqd1(u*2,l,m,x,p);
else uqd1(u*2+1,m+1,r,x,p);
pushup(u);
}
// 单点查询
int q1(int u,int l,int r,int p){
if(l==r)return w[u];
pushdown(u,l,r);
int m=(l+r)/2;
if(p<=m)return q1(u*2,l,m,p);
else return q1(u*2+1,m+1,r,p);
}
// 区间加法
void uqd2(int u,int l,int r,int L,int R,int x){
if(in(l,r,L,R)){
w[u]=(w[u]+1LL*(r-l+1)*x%p)%p;
lzj[u]=(lzj[u]+x)%p;
return;
}
if(of(l,r,L,R))return;
pushdown(u,l,r);
int m=(l+r)/2;
uqd2(u*2,l,m,L,R,x);
uqd2(u*2+1,m+1,r,L,R,x);
pushup(u);
}
// 区间乘法
void uqd3(int u,int l,int r,int L,int R,int x){
if(in(l,r,L,R)){
w[u]=1LL*w[u]*x%p;
lzc[u]=1LL*lzc[u]*x%p;
lzj[u]=1LL*lzj[u]*x%p;
return;
}
if(of(l,r,L,R))return;
pushdown(u,l,r);
int m=(l+r)/2;
uqd3(u*2,l,m,L,R,x);
uqd3(u*2+1,m+1,r,L,R,x);
pushup(u);
}
// 区间查询
int q2(int u,int l,int r,int L,int R){
if(in(l,r,L,R))return w[u];
if(of(l,r,L,R))return 0;
pushdown(u,l,r);
int m=(l+r)/2;
return (q2(u*2,l,m,L,R)+q2(u*2+1,m+1,r,L,R))%p;
}
int main(){
int n,q,m;
cin>>n>>q>>m;
p=m; // 设置模数
for(int i=1;i<=n;++i){
cin>>a[i];
}
build(1,1,n); // 构建线段树
while(q--){
int op,x,y,k;
cin>>op;
if(op==1){ // 区间乘法
cin>>x>>y>>k;
uqd3(1,1,n,x,y,k);
}else if(op==2){ // 区间加法
cin>>x>>y>>k;
uqd2(1,1,n,x,y,k);
}else if(op==3){ // 区间查询
cin>>x>>y;
cout<<q2(1,1,n,x,y)<<endl;
}
}
}
朴素 BIT
#include<bits/stdc++.h>
using namespace std;
int a[500005],c[500005],n; // a:原始数组 c:树状数组 n:数组大小
// 计算lowbit,获取x二进制表示中最低位的1
inline int lowbit(int x){
return x&(-x);
}
// 树状数组单点更新:在位置a加上值b
inline void add(int a,int b){
for (int i=a;i<=n;i+=lowbit(i))c[i]+=b; // 向上更新所有相关节点
}
// 树状数组前缀和查询:求[1,i]区间和
inline int sum(int i){
int cnt=0;
for (int j=i;j>=1;j-=lowbit(j))cnt+=c[j]; // 向下累加所有相关节点
return cnt;
}
int main(){
int m;
cin>>n>>m; // 输入数组大小n和操作次数m
for (int i=1;i<=n;++i){
cin>>a[i]; // 读入原始数组
add(i,a[i]); // 初始化树状数组
}
while (m--){
int op;cin>>op; // 输入操作类型
if (op==1){ // 操作1:单点修改
int x,k;cin>>x>>k;
add(x,k); // 在位置x加上k
}
else { // 操作2:区间查询
int l,r;cin>>l>>r;
cout<<sum(r)-sum(l-1)<<'\n'; // 输出[l,r]区间和
}
}
}
朴素 Trie
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3000005;
int trie[MAXN][62]; // Trie树节点数组,每个节点有62个子节点(数字+大小写字母)
int cnt[MAXN]; // 记录以每个节点为结尾的字符串数量
int tot; // Trie树节点计数器
// 字符映射函数:将字符转换为0-61的索引
int charid(char c){
if (c>='0'&&c<='9')return c-'0'; // 数字0-9映射到0-9
if (c>='A'&&c<='Z')return c-'A'+10; // 大写字母映射到10-35
if (c>='a'&&c<='z')return c-'a'+36; // 小写字母映射到36-61
}
// 向Trie树中插入字符串
void insert(string s){
int u=0; // 从根节点开始
for (int i=0;i<(int)s.length();++i){
int a=charid(s[i]); // 获取当前字符的索引
if(trie[u][a]==0)trie[u][a]=++tot; // 如果子节点不存在则创建新节点
u=trie[u][a]; // 移动到子节点
cnt[u]++; // 增加经过该节点的字符串计数
}
}
// 查询字符串在Trie树中的出现次数
int uqd(string s){
int u=0; // 从根节点开始
for (int i=0;i<(int)s.length();++i){
int a=charid(s[i]); // 获取当前字符的索引
if(trie[u][a]==0)return 0; // 如果路径不存在则返回0
u=trie[u][a]; // 移动到子节点
}
return cnt[u]; // 返回以该节点为结尾的字符串数量
}
// 处理每组测试数据
void solve(){
tot=0; // 重置节点计数器
int n,q;cin>>n>>q;
for (int i=1;i<=n;++i){
string s;cin>>s;
insert(s); // 插入所有字符串
}
for (int i=1;i<=q;++i){
string s;cin>>s;
cout<<uqd(s)<<'\n'; // 查询每个字符串
}
// 清空Trie树,为下一组测试数据做准备
for(int i=0;i<=tot;i++){
memset(trie[i],0,sizeof(trie[i]));
cnt[i]=0;
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int T;cin>>T;
while (T--)solve(); // 处理T组测试数据
}
0/1 Trie
#include<bits/stdc++.h>
using namespace std;
const int N=5000005;
int trie[N][2],a[1000005],tot; // trie:01字典树 a:输入数组 tot:节点计数
// 向01字典树中插入整数x
void insert(int x){
int p=0; // 从根节点开始
for (int i=(1<<30);i;i>>=1){ // 从最高位(第30位)到最低位遍历
int a=(x&i)?1:0; // 提取当前位的值(0或1)
if (!trie[p][a])trie[p][a]=++tot; // 如果路径不存在则创建新节点
p=trie[p][a]; // 移动到子节点
}
}
// 查询与x进行异或能得到的最大值
int query(int x){
int p=0,cnt=0; // p:当前节点 cnt:累计的异或值
for (int i=(1<<30);i;i>>=1){ // 从最高位到最低位遍历
int a=(x&i)?1:0; // 提取x当前位的值
if (trie[p][a^1]){ // 优先选择与当前位相反的路径(使异或结果最大)
cnt+=i; // 当前位异或结果为1,累加到结果中
p=trie[p][a^1]; // 移动到相反分支
}
else p=trie[p][a]; // 相反分支不存在,只能走相同分支
}
return cnt; // 返回最大异或值
}
int main(){
int n;cin>>n;
for (int i=1;i<=n;++i){
cin>>a[i];
insert(a[i]); // 将所有数字插入01字典树
}
int maxn=0;
for (int i=1;i<=n;++i)maxn=max(maxn,query(a[i])); // 查询每个数字能得到的最大异或值
cout<<maxn; // 输出所有最大异或值中的最大值
}
手写堆
#include<bits/stdc++.h>
#define int long long
using namespace std;
int w[1000005],cnt=0; // w:堆数组 cnt:堆中元素个数
// 获取堆顶元素(最小值)
int top(){
return w[1];
}
// 插入后的上浮调整(维护小顶堆性质)
void modify(int u){// 插入后的修复
if (u==1)return; // 到达根节点,停止
if (w[u]>w[u/2])return; // 当前节点值大于父节点,满足小顶堆性质,停止
swap(w[u],w[u/2]); // 当前节点值小于父节点,交换
modify(u/2); // 递归向上调整
}
// 向堆中插入元素
void push(int x){// 插入
w[++cnt]=x; // 将新元素放到堆的末尾
modify(cnt); // 从新元素位置开始向上调整
}
// 删除后的下沉调整(维护小顶堆性质)
void repair(int u){// 删除后的修复
int t = u; // t记录最小值的节点下标
// 与左子节点比较
if (u*2<=cnt && w[u*2]<w[t]) t = u*2;
// 与右子节点比较
if (u*2+1<=cnt && w[u*2+1]<w[t]) t = u*2+1;
// 如果当前节点不是最小值,需要交换并继续调整
if (u!=t){
swap(w[u],w[t]);
repair(t); // 递归向下调整
}
}
// 删除堆顶元素
void pop(){// 删除
swap(w[1],w[cnt]);// 交换根节点和最后一个节点
w[cnt]=0;cnt--;// 删除最后一个节点(原堆顶)
repair(1); // 从根节点开始向下调整
}
signed main(){
int q;cin>>q; // 操作次数
while (q--){
int op;cin>>op; // 操作类型
if (op==1){ // 插入操作
int x;cin>>x;
push(x);
}
if (op==2){ // 查询堆顶操作
cout<<top()<<'\n';
}
if (op==3){ // 删除堆顶操作
pop();
}
}
}
强联通(缩点)
#include<bits/stdc++.h>
using namespace std;
struct edge{
int nxt,to; //nxt存储下一条边的索引,to存储这条边指向的节点
}e[200005];
int hd[10005],tot; //hd数组是头指针数组,hd[u]表示节点u的第一条边在e数组中的索引,tot是边的计数器
void add(int u,int v){
e[++tot].to=v; //新边的终点是v
e[tot].nxt=hd[u]; //新边的下一条边是原来u的第一条边
hd[u]=tot; //更新u的第一条边为新边
}
stack<int>sk; //Tarjan算法使用的栈
int scc[10005],sc,dfnc,dfn[10005],sz[10005],low[10005],vis[10005],w[10005],a[10005];
//scc[i]表示节点i属于哪个强连通分量,sc是强连通分量计数器
//dfnc是DFS序计数器,dfn[i]是节点i的DFS序编号
//sz[i]表示第i个强连通分量包含的节点数
//low[i]表示节点i能回溯到的最早的DFS序
//vis[i]标记节点i是否在栈中
//w[i]表示第i个强连通分量的总权值
//a[i]表示原图中节点i的权值
void tarjan(int u){
low[u]=dfn[u]=++dfnc,sk.push(u),vis[u]=1; //初始化low和dfn为DFS序,节点入栈,标记在栈中
for(int i=hd[u];i!=0;i=e[i].nxt){ //遍历u的所有出边
int v=e[i].to;
if(!dfn[v]){ //如果v节点还没有被访问过
tarjan(v); //递归处理v节点
low[u]=min(low[u],low[v]); //用v的low值更新u的low值
}
else if(vis[v]){ //如果v节点已经被访问过且在栈中
low[u]=min(low[u],dfn[v]); //用v的dfn值更新u的low值
}
}
if(dfn[u]==low[u]){ //如果u是强连通分量的根节点
sc++; //强连通分量计数器加1
while(1){
int v=sk.top();sk.pop(); //弹出栈顶节点
scc[v]=sc,sz[sc]++,vis[v]=0; //标记v属于第sc个强连通分量,该分量节点数加1,标记v不在栈中
if(v==u)break; //直到弹出u节点为止
}
}
}
edge e2[100005]; //新的边数组,用于存储缩点后的DAG
int hd2[10005],tot2,in[10005]; //hd2是新图的头指针,tot2是新图的边计数器,in记录每个强连通分量的入度
void add2(int u,int v){
e2[++tot2].to=v;
e2[tot2].nxt=hd2[u];
hd2[u]=tot2;
}
int ok[10005],f[10005]; //ok标记节点是否被访问过,f[i]表示从第i个强连通分量出发能获得的最大权值和
void dfs(int u){
f[u]=w[u];ok[u]=1; //初始化f[u]为当前强连通分量的权值,标记已访问
for(int i=hd2[u];i!=0;i=e2[i].nxt){ //遍历u的所有出边
int v=e2[i].to;
if(!ok[v]){
dfs(v); //递归处理v
}
f[u]=max(f[u],w[u]+f[v]); //更新从u出发能获得的最大权值和
}
}
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;++i)cin>>a[i]; //读入每个节点的权值
for(int i=1;i<=m;++i){
int u,v;cin>>u>>v;
add(u,v); //建立原图的有向边
}
for(int i=1;i<=n;++i)if(!dfn[i])tarjan(i); //对每个未访问的节点执行Tarjan算法求强连通分量
for(int u=1;u<=n;++u){
for(int i=hd[u];i!=0;i=e[i].nxt){
int v=e[i].to;
if(scc[u]!=scc[v])add2(scc[v],scc[u]),in[scc[u]]++; //建立缩点后的DAG,注意方向反转
}
}
for(int i=1;i<=n;++i){
w[scc[i]]+=a[i]; //计算每个强连通分量的总权值
}
for(int i=1;i<=sc;++i){
if(!in[i]){ //从入度为0的强连通分量开始DFS
dfs(i);
}
}
int maxn=0;
for(int i=1;i<=sc;++i)maxn=max(maxn,f[i]); //找出所有强连通分量中的最大权值和
cout<<maxn;
}
KMP
#include<bits/stdc++.h>
using namespace std;
int b[2000005]; //KMP算法的部分匹配表(next数组)
int main() {
string s1,s2;cin>>s1>>s2; //s1是主串,s2是模式串
int j=0;
if (s2.length()>0) {
b[0]=0; //部分匹配表的第一个元素总是0
//构建模式串s2的部分匹配表
for (int i=1;i<s2.length();++i){
//当字符不匹配时,利用已计算的部分匹配值回溯
while (j>0&&s2[i]!=s2[j]){
j=b[j-1];
}
//如果字符匹配,增加匹配长度
if (s2[i]==s2[j])j++;
b[i]=j; //记录当前位置的部分匹配值
}
}
j=0; //重置匹配长度,用于主串匹配
//在主串s1中搜索模式串s2
for (int i=0;i<s1.length();++i){
//当字符不匹配时,利用部分匹配表回溯
while (j>0&&s1[i]!=s2[j])j=b[j-1];
//如果字符匹配,增加匹配长度
if (s2[j]==s1[i])j++;
//找到完整匹配
if (j==s2.length()){
//输出匹配起始位置(从1开始计数)
cout<<(i+1)-s2.length()+1<<'\n';
//继续搜索下一个可能的匹配
j=b[j-1];
}
}
//输出模式串的部分匹配表
for (int i=0;i<s2.length();++i)cout<<b[i]<<" ";
}
manacher
#include <bits/stdc++.h>
using namespace std;
const int N=2.2e7+5;
int r[N],m,n,ans=1; //r[i]表示以i为中心的最长回文半径,m是原串长度,n是新串长度,ans记录最长回文子串长度
char s[N],t[N]; //s是原字符串,t是预处理后的新字符串
int main() {
cin>>s+1,m=strlen(s+1); //从下标1开始读入字符串s,并计算长度m
t[0]='#',t[++n]='#'; //新字符串t开头添加特殊字符'#'
for (int i=1;i<=m;++i)t[++n]=s[i],t[++n]='#'; //将原字符串每个字符间插入'#'
t[++n]='$',r[1]=1; //新字符串结尾添加'$',初始化r[1]=1
//Manacher算法核心部分
for (int i=2,c=1,R=1;i<n;++i){ //c表示当前回文中心,R表示当前回文右边界
r[i]=min(R-i+1,r[2*c-i]); //利用对称性初始化r[i]
while (t[i-r[i]]==t[i+r[i]])r[i]++; //中心扩展法
if (i+r[i]-1>R)c=i,R=i+r[i]-1; //更新回文中心和右边界
ans=max(ans,r[i]-1); //更新最长回文子串长度(r[i]-1即为原串中对应回文长度)
}
cout<<ans; //输出最长回文子串长度
return 0;
}
Dij
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
struct edge{
int nxt,to,w; //nxt:下一条边的索引 to:边的终点 w:边的权重
}e[2000005];
int hd[1000005],tot; //hd:头指针数组 tot:边计数器
void add(int u,int v,int w){ //添加从u到v权重为w的有向边
e[++tot].to=v;
e[tot].nxt=hd[u];
e[tot].w=w;
hd[u]=tot;
}
int dis[1000005]; //dis[i]表示从源点s到i的最短距离
priority_queue<pii,vector<pii>,greater<pii>> q; //小顶堆,存储(距离,节点)
int main(){
int n,m,s;cin>>n>>m>>s; //n:节点数 m:边数 s:源点
for (int i=1;i<=m;++i){
int u,v,w;cin>>u>>v>>w;
add(u,v,w); //建图
}
memset(dis,0x3f,sizeof dis); //初始化所有距离为无穷大
dis[s]=0; //源点到自己的距离为0
q.push(make_pair(0,s)); //将源点加入优先队列
while (!q.empty()){
auto t=q.top();q.pop(); //取出当前距离最小的节点
int u=t.second;
if (t.first!=dis[u])continue; //如果该节点的距离已被更新过,跳过
for (int i=hd[u];i!=0;i=e[i].nxt){ //遍历u的所有出边
int v=e[i].to,d=e[i].w+t.first; //计算经过u到v的新距离
if (d<dis[v]){ //如果找到更短的路径
dis[v]=d; //更新距离
q.push(make_pair(d,v)); //将新距离加入优先队列
}
}
}
for (int i=1;i<=n;++i)cout<<dis[i]<<" "; //输出从源点到所有节点的最短距离
}
割点
判定:对于某个顶点 \(u\),如果存在至少一个顶点 \(v\)(\(u\) 的儿子),使得 \(low_v \geq dfn_u\),即不能回到祖先,那么 \(u\) 点为割点。
#include<bits/stdc++.h>
using namespace std;
stack<int>sk; //Tarjan算法使用的栈
struct edge{
int to,nxt; //to:边的终点 nxt:下一条边的索引
}e[400005];
int hd[200005],cnt; //hd:头指针数组 cnt:边计数器
int dfn[200005],dfnc,low[200005],vis[200005]; //dfn:DFS序 low:能回溯到的最早DFS序 vis:是否在栈中
set<int>st; //存储割点集合(自动去重)
int rt; //当前DFS树的根节点
void add(int u,int v){ //添加无向边
e[++cnt]={v,hd[u]};
hd[u]=cnt;
}
void tarjan(int u){ //Tarjan算法求割点
low[u]=dfn[u]=++dfnc; //初始化low和dfn
sk.push(u); //节点入栈
vis[u]=1; //标记在栈中
int num=0; //记录u的子树数量
for(int i=hd[u];i!=0;i=e[i].nxt){ //遍历u的所有邻接点
int v=e[i].to;
if(!dfn[v]){ //如果v未被访问
tarjan(v); //递归处理v
low[u]=min(low[u],low[v]); //更新u的low值
if(low[v]>=dfn[u]){ //如果v不能绕过u到达更早的节点
if(u!=rt){ //且u不是根节点
st.insert(u); //u是割点
}
num++; //子树数量增加
}
}
else if(vis[v]){ //如果v已被访问且在栈中(后向边)
low[u]=min(low[u],dfn[v]); //更新u的low值
}
}
if(u==rt&&num>=2){ //如果是根节点且有至少2棵子树
st.insert(u); //根节点是割点
}
}
int main(){
int n,m;cin>>n>>m; //n:节点数 m:边数
for(int i=1;i<=m;++i){
int u,v;cin>>u>>v;
add(u,v),add(v,u); //建立无向图
}
for(int i=1;i<=n;++i){ //遍历所有节点
if(!dfn[i]){ //如果节点i未被访问
rt=i; //设置当前DFS树的根节点
tarjan(i); //从i开始Tarjan算法
}
}
cout<<st.size()<<'\n'; //输出割点数量
for(auto i:st)cout<<i<<' '; //输出所有割点
}
LCA
e,给个核心得了
//初始化ST表,进行倍增预处理
void init(){
for(int j=1;j<=19;++j){ //j从1到19,表示2^j级祖先
for(int i=1;i<=n;++i){ //遍历每个节点
f[i][j]=f[f[i][j-1]][j-1]; //节点i的2^j级祖先是其2^(j-1)级祖先的2^(j-1)级祖先
}
}
}
//求节点u和v的最近公共祖先(LCA)
int lca(int u,int v){
//保证u的深度不小于v的深度
if(dep[u]<dep[v])swap(u,v);
//将u提升到与v同一深度
for(int j=19;j>=0;--j){ //从大到小尝试跳跃
if(dep[f[u][j]]>=dep[v])u=f[u][j]; //如果跳跃后深度仍不小于v,则进行跳跃
}
//如果此时u和v相同,说明v就是u的祖先,直接返回
if(u==v)return u;
//同时提升u和v,保持它们在同一深度但祖先不同
for(int j=19;j>=0;--j){ //从大到小尝试跳跃
if(f[u][j]!=f[v][j]){ //如果跳跃后祖先不同,说明还没到LCA,可以跳跃
u=f[u][j];
v=f[v][j];
}
}
//此时u和v的父节点就是LCA
return f[u][0];
}
exgcd
//扩展欧几里得算法:求解ax + by = gcd(a,b)的一组整数解(x,y)
void exgcd(int a,int b,int& x,int& y){
if(b==0){ //递归终止条件:b为0时,gcd(a,b)=a
x=1,y=0;return; //此时方程的解为x=1,y=0(a*1 + 0*0 = a)
}
exgcd(b,a%b,x,y); //递归求解,参数变为(b, a%b)
int _y=x-(a/b)*y; //利用递归结果计算当前层的y值
x=y,y=_y; //更新x和y为当前层的解
}
费马小定理
对于任意互质 \(a,p\),有 \(a^{p-1}≡ 1 \pmod p\)。
扩展欧拉定理
其中 \(\phi(n)\) 为欧拉函数。
更一般地,对于任意整数 \(a,m,m \geq 1\) 和 $b \geq \phi(m) $:
\[a^b \equiv a^{b \bmod \phi(m) + \phi(m)} \pmod{m}
\]
(当 \(\gcd(a,m)=1\) 时,可去掉 $ +\phi(m) $ 项,即退化为欧拉定理形式。)
\(phi(n)\) 求法(一定必背的!!!):
//欧拉函数:计算小于等于n且与n互质的正整数的个数
int oula(int n){
int ans=n; //初始化答案为n
for(int i=2;i*i<=n;++i){ //遍历所有可能的质因子i(从2到sqrt(n))
if(n%i==0){ //如果i是n的质因子
ans-=ans/i; //根据欧拉函数公式:ans = ans * (1 - 1/i)
while(n%i==0)n/=i; //将n中的所有i因子去除
}
}
if(n>1)ans-=ans/n; //如果n还有大于sqrt(n)的质因子,同样处理
return ans; //返回欧拉函数值
}
exCRT
#include<bits/stdc++.h>
#define int __int128 //使用128位整数
using namespace std;
//快速读入函数(优化输入)
inline int read(){
int x=0,f=1;
char ch=getchar_unlocked();
while(!isdigit(ch)){
if(ch=='-')
f=-1;
ch=getchar_unlocked();
}
while(isdigit(ch)){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar_unlocked();
}
return x*f;
}
//快速输出函数(优化输出)
inline void write(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)write(x/10);
putchar(x%10+'0');
}
//扩展欧几里得算法:求解ax+by=gcd(a,b)
void exgcd(int a,int b,int &x,int &y){
if(!b){x=1,y=0;return;}
exgcd(b,a%b,y,x);y-=a/b*x;
}
signed main(){
int n,a1,b1,a2,b2,x,y;n=read(),a1=read(),b1=read(); //读入n和第一个方程的参数
for(int i=2;i<=n;++i){ //依次处理每个方程
a2=read(),b2=read(); //读入当前方程的参数
int d=__gcd(a1,a2),c=(b2-b1)%a2; //d是a1,a2的最大公约数,c是常数项差
if(c%d){cout<<-1;return 0;} //如果c不能被d整除,无解
exgcd(a1,a2,x,y); //求解a1*x+a2*y=d
int lcm=a1/d*a2; //计算a1和a2的最小公倍数
//更新解:b1 = 当前解 + k*lcm,取最小非负解
b1=((1ll*x*(c/d)%a2*a1+b1)%lcm+lcm)%lcm;
a1=lcm; //更新模数为新的最小公倍数
}
write(b1); //输出最终解
}
高斯消元
给定一个线性方程组,对其求解。
\[\begin{cases} a_{1, 1} x_1 + a_{1, 2} x_2 + \cdots + a_{1, n} x_n = b_1 \\ a_{2, 1} x_1 + a_{2, 2} x_2 + \cdots + a_{2, n} x_n = b_2 \\ \cdots \\ a_{n,1} x_1 + a_{n, 2} x_2 + \cdots + a_{n, n} x_n = b_n \end{cases}
\]
#include<bits/stdc++.h>
using namespace std;
double a[105][105]; //增广矩阵,a[i][n+1]存储常数项
const double eps=0.0000001; //精度阈值,用于判断浮点数是否为0
int n;
//高斯消元法求解线性方程组,返回是否有解
bool guess(){
for(int i=1;i<=n;++i){ //遍历每一列
//选主元:在当前列i中从第i行开始往下找第一个非零元素
int r=i;
for(int k=i;k<=n;++k){
if(fabs(a[k][i])>eps){ //找到第一个非零元素
r=k;
break;
}
}
if(r!=i)swap(a[i],a[r]); //将找到的行与当前行交换
if(fabs(a[i][i])<eps)return 0; //如果主元为0,说明无解或有无穷多解
//将主元化为1,同时对该行其他元素进行相应操作
for(int j=n+1;j>=i;--j){ //从右往左处理,避免提前覆盖主元
a[i][j]/=a[i][i];
}
//消元:用当前行消去下面各行在第i列的元素
for(int k=i+1;k<=n;++k){
for(int j=n+1;j>=i;--j){ //从右往左处理
a[k][j]-=a[i][j]*a[k][i];
}
}
}
//回代求解
for(int i=n-1;i>=1;--i){ //从最后一行往前回代
for(int j=i+1;j<=n;++j){
a[i][n+1]-=a[i][j]*a[j][n+1]; //减去已知变量的贡献
}
}
return 1; //求解成功
}
int main(){
cin>>n; //输入方程个数(未知数个数)
for(int i=1;i<=n;++i){
for(int j=1;j<=n+1;++j){
cin>>a[i][j]; //读入增广矩阵
}
}
if(!guess())cout<<"No Solution"; //无解
else {
for(int i=1;i<=n;++i){
cout<<fixed<<setprecision(2)<<a[i][n+1]<<'\n'; //输出解,保留2位小数
}
}
}
矩阵加速
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7;
struct mar{
int a[5][5]; //3x3矩阵结构体
};
//矩阵乘法
mar mul(mar x,mar y){
mar c;
memset(c.a,0,sizeof(c.a)); //初始化结果矩阵为0
for(int i=1;i<=3;++i){
for(int j=1;j<=3;++j){
for(int k=1;k<=3;++k){
c.a[i][j]=(c.a[i][j]+x.a[i][k]*y.a[k][j]%mod)%mod; //矩阵乘法并取模
}
}
}
return c;
}
//矩阵快速幂
mar qpow(mar mart,int b){
mar res;
memset(res.a,0,sizeof(res.a));
for(int i=1;i<=3;++i)res.a[i][i]=1; //初始化为单位矩阵
while(b){
if(b&1)res=mul(res,mart); //如果当前二进制位为1,乘上对应矩阵
mart=mul(mart,mart); //矩阵平方
b>>=1; //指数右移
}
return res;
}
signed main(){
int T;cin>>T; //测试数据组数
while(T--){
int n;cin>>n;
if(n<=3){cout<<"1\n";continue;} //前3项直接输出1
//构造递推矩阵:[f(n), f(n-1), f(n-2)] = [f(n-1), f(n-2), f(n-3)] * M
//递推关系:f(n) = f(n-1) + f(n-3)
mar cnt;
cnt.a[1][1]=1,cnt.a[1][2]=1,cnt.a[1][3]=0; //第一行:f(n)=1*f(n-1)+1*f(n-2)+0*f(n-3)
cnt.a[2][1]=0,cnt.a[2][2]=0,cnt.a[2][3]=1; //第二行:f(n-1)=0*f(n-1)+0*f(n-2)+1*f(n-3)
cnt.a[3][1]=1,cnt.a[3][2]=0,cnt.a[3][3]=0; //第三行:f(n-2)=1*f(n-1)+0*f(n-2)+0*f(n-3)
mar res=qpow(cnt,n-3); //计算矩阵的(n-3)次幂
//初始状态:[f(3), f(2), f(1)] = [1, 1, 1]
//结果 = 1*f(3) + 1*f(2) + 1*f(1) = res.a[1][1]*1 + res.a[1][2]*1 + res.a[1][3]*1
cout<<(res.a[1][1]+res.a[1][2]+res.a[1][3])%mod<<'\n';
}
}
图上/树上 矩阵加速
一样的思想,只不过初始矩阵变成了图/树的邻接矩阵。

浙公网安备 33010602011771号