(待更新)NOIP 模板

NOIP 模板汇总

*P3382 三分

#include<bits/stdc++.h>
using namespace std;
const double eps=1e-6;
int n;
double l,r,a[20];
double f(double x){
    double ans=a[0],y=x;
    for(int i=1;i<=n;i++){
        ans+=y*a[i];
        y*=x;
    }
    return ans;
}
int main(){
    cin>>n>>l>>r;
    for(int i=n;i>=0;i--)cin>>a[i];
    while(r-l>eps){
        double midl=l+(r-l)/3.0;
        double midr=r-(r-l)/3.0;
        if(f(midl)<f(midr))l=midl;
        else r=midr;
    }
    printf("%.6f",l);
    return 0;
}

P1886 单调队列

以求最大值为例:维护一个下标单调递增、权值单调递减的队列,依次加入元素,如果加入的元素大于队尾元素,就把队尾弹出(因为队尾先出滑动窗口,而且比新加入的元素小,再也没办法作为最大值了),如果队头队尾的差超过窗口大小,弹出超出范围队首。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,k,head,tail;
int a[N],q[N];
int main(){
	cin>>n>>k;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){
        while(head<=tail&&a[q[tail]]>=a[i])tail--;
        q[++tail]=i;
        while(head<=tail&&i-q[head]+1>k)head++;
        if(i>=k)cout<<a[q[head]]<<" ";
    }
    cout<<endl;
    head=0,tail=0;
    for(int i=1;i<=n;i++){
        while(head<=tail&&a[q[tail]]<=a[i])tail--;
        q[++tail]=i;
        while(head<=tail&&i-q[head]+1>k)head++;
        if(i>=k)cout<<a[q[head]]<<" ";
    }
	return 0;
} 

*P5788 单调栈

#include<bits/stdc++.h>
using namespace std;
const int N=3000005;
int n,a[N],f[N];
int st[N],top;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){
        while(top&&a[st[top]]<a[i]){
            f[st[top]]=i;
            top--;
        }
        st[++top]=i;
    }
    for(int i=1;i<=n;i++){
        cout<<f[i]<<" ";
    }
    return 0;
}

*P5854 笛卡尔树

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e7+10;
int n,top;
int p[N],st[N];
int ls[N],rs[N];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie();
    cin>>n;
    for(int i=1;i<=n;i++)cin>>p[i];
    for(int i=1;i<=n;i++){
        int k=top;
        while(k>0&&p[st[k]]>p[i])k--;
        if(k)rs[st[k]]=i;
        if(k<top)ls[i]=st[k+1];
        st[++k]=i;
        top=k;
    }
    int ans1=0,ans2=0;
    for(int i=1;i<=n;i++){
        ans1^=i*(ls[i]+1);
        ans2^=i*(rs[i]+1);
    }
    cout<<ans1<<" "<<ans2;
    return 0;
}

*P3366 最小生成树

#include<bits/stdc++.h>
using namespace std;
#define int long long
struct node{
    int a,b,c;
};
bool cmp(node x,node y){
    return x.c<y.c;
}
int n,m;
node e[500005];
int p[5005];
int find(int x){
    if(x!=p[x])p[x]=find(p[x]);
    return p[x];
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>e[i].a>>e[i].b>>e[i].c;
    }
    sort(e+1,e+m+1,cmp);
    for(int i=1;i<=n;i++)p[i]=i;
    int ans=0;
    for(int i=1;i<=m;i++){
        int a=e[i].a,b=e[i].b,c=e[i].c;
        if(find(a)!=find(b)){
            p[find(a)]=p[b];
            ans+=c;
        }
    }
    for(int i=1;i<=n;i++){
        if(find(1)!=find(i)){
            cout<<"orz";
            return 0;
        }
    }
    cout<<ans;
    return 0;
}

P3379 最近公共祖先 LCA

1.倍增法

记 \(fa[i][j]\) 为结点 i 向上走 \(2^j\) 个结点的结点编号,\(d[i]\) 表示结点i的深度,用 dfs 函数求出这两个数组的值。在 lca 函数中我们先让两个结点深度相同,再依次尝试让两个结点同时向上走 \(2^j\) 个结点,且两个结点不同(即lca在两点上面),最终两个结点的父亲结点就是 lca。

image-20250810184630154image-20250810184924973

时间复杂度:预处理 \(O(n)\),查询 \(O(\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,m,s;
vector<int>v[N];
int fa[N][50],d[N];
void dfs(int a,int b){
	d[a]=d[b]+1;
	fa[a][0]=b;
	for(int i=1;i<=30;i++){
		fa[a][i]=fa[fa[a][i-1]][i-1];
	}
	for(int i:v[a]){
		if(i!=b) dfs(i,a);
	}
}
int lca(int a,int b){
	if(d[a]<d[b]) swap(a,b);
	for(int i=30;i>=0;i--){
		if(d[fa[a][i]]>=d[b]) a=fa[a][i];
	}
	if(a==b) return a;
	for(int i=30;i>=0;i--){
		if(fa[a][i]!=fa[b][i]){
			a=fa[a][i];
			b=fa[b][i];
		}
	}
	return fa[a][0];
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m>>s;
	n--;
	while(n--){
		int x,y;
		cin>>x>>y;
		v[x].push_back(y);
		v[y].push_back(x);
	}
	dfs(s,0);
	while(m--){
		int x,y;
		cin>>x>>y;
		cout<<lca(x,y)<<"\n";
	}
	return 0;
} 

2.树链剖分

在树上找出若干条链(如图1),从根节点开始,找出重儿子(子树大小最大的儿子)向下连,一直到叶子节点,轻儿子重新开始一条链。当两个结点在一条链上,lca 就是较浅的点,否则直接跳到两个结点链顶的父亲结点(如图2)。

image-20250810185927578image-20250810191438167

时间复杂度:预处理\(O(n)\),查询\(O(\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,s,x,y;
vector<int>v[N];
int fu[N],dep[N],siz[N],son[N],top[N];
void dfs(int u,int fa){
    siz[u]=1;fu[u]=fa;
    dep[u]=dep[fa]+1;
    for(auto i:v[u]){
        if(i==fa)continue;
        dfs(i,u);
        siz[u]+=siz[i];
        if(siz[i]>siz[son[u]])son[u]=i;
    }
}
void dfs1(int u,int fa){
    top[u]=fa;
    if(son[u])dfs1(son[u],fa);
    for(auto i:v[u]){
        if(top[i])continue;
        dfs1(i,i);
    }
}
int lca(int x,int y){
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        x=fu[top[x]];
    }
    if(dep[x]<dep[y])return x;
    else return y;
}
int main(){
    cin>>n>>m>>s;
    for(int i=1;i<n;i++){
        cin>>x>>y;
        v[x].push_back(y);
        v[y].push_back(x);
    }
    dfs(s,0);
    dfs1(s,s);
    for(int i=1;i<=m;i++){
        cin>>x>>y;
        cout<<lca(x,y)<<endl;
    }
    return 0;
}

P4779 单源最短路径

我们把未处理的点都标为白色,已处理的点标为蓝色,从结点 s 开始处理。

设计一个 dis 数组,记录从 1 到 i 的最短路,初始 \(dist[s]=0\),其余设为正无穷。

在未处理的白点中,每次找一个dist值最小的点,把这个点变成蓝色,对连接这个点的所有白点进行更新,从点 u 走到白点 v,白点到 1 的距离是 \(dis[u]+dis(u,v)\),把这个值和白点原来的 dis 值比较并更新。

正确性证明:当所有边长都是非负数的时候,全局最小值不可能再被其他节点更新,所以找出的蓝点 x 必然满足:\(dis[x]\)已经是起点到 x 的最短路径,我们不断选择全局最小值进行标记和拓展,最终可以得到起点到每个节点的最短路径的长度。

限制:无法处理负边权

时间复杂度 \(O(n^2)\),性能较差

优化:在找到dist值最小的白点时,可以用堆优化用 logn 的复杂度取出堆顶并删除,用 logn 的复杂度遍历每条边,总复杂度\(O((n+m) \log n)\)。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define x first
#define y second
const int N=1e5+10;
int n,m,s;
int vis[N],dis[N];
vector<pair<int,int>>ve[N];
priority_queue<pair<int,int>>pq;
signed main(){
    cin>>n>>m>>s;
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        ve[u].push_back({v,w});
    }
    pq.push({0,s});
    memset(dis,0x3f3f3f,sizeof dis);
    dis[s]=0;
    while(!pq.empty()){
        pair<int,int>top=pq.top();
        pq.pop();
        if(vis[top.y])continue;
        vis[top.y]=1;
        for(pair<int,int>v:ve[top.y]){
            if(dis[v.x]>dis[top.y]+v.y){
                dis[v.x]=dis[top.y]+v.y;
                pq.push({-dis[v.x],v.x});
            }
        }
    }
    for(int i=1;i<=n;i++)cout<<dis[i]<<" ";
    return 0;
}

*P3385 负环

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define x first
#define y second
const int N=3e3+10;
int n,m;
int vis[N],dis[N],cnt[N];
vector<pair<int,int>>ve[N];
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>pq;
signed main(){
    int T;
    cin>>T;
    while(T--){
        cin>>n>>m;
        for(int i=1;i<=n;i++) ve[i].clear();
        for(int i=1;i<=m;i++){
            int u,v,w;
            cin>>u>>v>>w;
            ve[u].push_back({v,w});
            if(w>=0)ve[v].push_back({u,w});
        }
        memset(dis,0x3f3f3f3f,sizeof dis);
        memset(vis,0,sizeof vis);
        memset(cnt,0,sizeof cnt);
        dis[1]=0;
        vis[1]=1;
        cnt[1]=1;
        while(!pq.empty())pq.pop();
        pq.push({0,1});
        bool flag=0;
        while(!pq.empty()){
            auto top=pq.top();
            pq.pop();
            int u=top.y;
            vis[u]=0;
            for(pair<int,int>v:ve[u]){
                if(dis[v.x]>dis[u]+v.y){
                    dis[v.x]=dis[u]+v.y;
                    cnt[v.x]=cnt[u]+1;
                    if(cnt[v.x]>n){
                        flag=1;
                        break;
                    }
                    if(!vis[v.x]){
                        pq.push({dis[v.x],v.x});
                        vis[v.x]=1;
                    }
                }
            }
            if(flag) break;
        }
        cout<<(flag?"YES":"NO")<<"\n";
    }
    return 0;
}

**P5960 差分约束

P3372 线段树 1

我们维护这样一个线段树,把1~n不断一分为二,序列左半部分对应结点编号乘2,右半部分对应乘2加1,其中结点 1 存储1~n 的和,build()用于建树。

image-20250808202647517

区间修改需要运用懒标记,每次修改区间,只将对应区间的懒标记增加,之后如果用到这个区间的子节点,就需要将懒标记下放,注意懒标记存储的是区间内每个数应该加的数量,所以计算和的时候要乘上对应序列的长度,如图:

image-20250809081204346

query()查询也类似,从根节点开始,对于他的左右子节点,如果包含相应的部分,就需要向下寻找,知道区间全部被包含,累加值。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mid ((l+r)>>1)
#define ls (u<<1)
#define rs ((u<<1)|1)
const int N=1e5+10;
int n,m;
int a[N];
struct tree{
    int sum,tag;
}tr[N*4];
void push_down(int u,int l,int r){
    tr[ls].sum+=tr[u].tag*(mid-l+1);
    tr[rs].sum+=tr[u].tag*(r-(mid+1)+1);
    tr[ls].tag+=tr[u].tag;
    tr[rs].tag+=tr[u].tag;
    tr[u].tag=0;
}
void push_up(int u){
    tr[u].sum=tr[ls].sum+tr[rs].sum;
}
void build(int u,int l,int r){
    if(l==r){
        tr[u].sum=a[l];
        return;
    }
    build(ls,l,mid);
    build(rs,mid+1,r);
    push_up(u);
}
void update(int u,int l,int r,int L,int R,int k){
    if(L<=l&&r<=R){
        tr[u].sum+=k*(r-l+1);
        tr[u].tag+=k;
        return;
    }
    push_down(u,l,r);
    if(L<=mid)update(ls,l,mid,L,R,k);
    if(R>mid)update(rs,mid+1,r,L,R,k);
    push_up(u);
}
int query(int u,int l,int r,int L,int R){
    if(L<=l&&r<=R)return tr[u].sum;
    push_down(u,l,r);
    int ans=0;
    if(L<=mid)ans+=query(ls,l,mid,L,R);
    if(R>mid)ans+=query(rs,mid+1,r,L,R);
    return ans;
}
signed main(){
	cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    build(1,1,n);
    while(m--){
        int op;
        cin>>op;
        if(op==1){
            int x,y,k;
            cin>>x>>y>>k;
            update(1,1,n,x,y,k);
        }else{
            int x,y;
            cin>>x>>y;
            cout<<query(1,1,n,x,y)<<endl;
        }
    }
	return 0;
} 

*P13825 线段树 1.5

#include<bits/stdc++.h>
#define mid (l+r)/2
#define int unsigned long long
using namespace std;
const int N=5e6;
int n,m,op,l,r,k,cnt,rt;
int a[N],laz[N],ls[N],rs[N];
void push_down(int u,int l,int r){
    if(laz[u]){
        if(!ls[u])ls[u]=++cnt;
        if(!rs[u])rs[u]=++cnt;
        a[ls[u]]+=(mid-l+1)*laz[u];
        a[rs[u]]+=(r-mid)*laz[u];
        laz[ls[u]]+=laz[u];
        laz[rs[u]]+=laz[u];
        laz[u]=0;
    }
}
void push_up(int u){
    a[u]=a[ls[u]]+a[rs[u]];
}
void update(int &u,int l,int r,int x,int y,int k){
    if(!u)u=++cnt;
    if(x<=l&&r<=y){
        a[u]+=k*(r-l+1);
        laz[u]+=k;
        return;
    }
    push_down(u,l,r);
    if(x<=mid)update(ls[u],l,mid,x,y,k);
    if(y>mid)update(rs[u],mid+1,r,x,y,k);
    push_up(u);
}
int query(int u,int l,int r,int x,int y){
    if(!u)return 0;
    if(x<=l&&r<=y)return a[u];
    push_down(u,l,r);
    int ans=0;
    if(x<=mid)ans+=query(ls[u],l,mid,x,y);
    if(y>mid)ans+=query(rs[u],mid+1,r,x,y);
    return ans;
}
signed main(){
    cin>>n>>m;
    while(m--){
        cin>>op;
        if(op==1){
            cin>>l>>r>>k;
            update(rt,1,n,l,r,k);
        }else{
            cin>>l>>r;
            cout<<query(rt,1,n,l,r)+(l+r)*(r-l+1)/2<<"\n";
        }
    }
    return 0;
}

P3373 线段树 2

需要维护两个懒标记,tag 和 tmg 分别维护乘和加,注意在乘的时候乘法和加法懒标记都要与之相乘,加的时候只有加法懒标记要相加,乘法懒标记要初始化为 1。

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+100;
#define ls (u*2)
#define rs (u*2+1)
#define mid ((l+r)/2)
#define int long long
long long summ[N*4],tag[N*4],tmg[N*4];
int n,q,mod,op,x,y,z,a[N];
void build(int u,int l,int r){
    tmg[u]=1;
    if(l==r){
        summ[u]=a[l]%mod;
        return;
    }
    build(ls,l,mid);
    build(rs,mid+1,r);
    summ[u]=(summ[ls]+summ[rs])%mod;
}
void push_down(int u,int l,int r){
    summ[ls]=(1ll*tmg[u]*summ[ls]%mod+1ll*tag[u]*(mid-l+1)%mod)%mod;
    summ[rs]=(1ll*tmg[u]*summ[rs]%mod+1ll*tag[u]*(r-mid)%mod)%mod;
    tmg[ls]=1ll*tmg[ls]*tmg[u]%mod;
    tmg[rs]=1ll*tmg[rs]*tmg[u]%mod;
    tag[ls]=(1ll*tag[ls]*tmg[u]%mod+tag[u])%mod;
    tag[rs]=(1ll*tag[rs]*tmg[u]%mod+tag[u])%mod;
    tag[u]=0;
    tmg[u]=1;
}
void add(int u,int l,int r,int L,int R,int k){
    if(L<=l&&R>=r){
        summ[u]=(summ[u]+1ll*k*(r-l+1)%mod)%mod;
        tag[u]=(tag[u]+k)%mod;
        return;
    }
    push_down(u,l,r);
    if(L<=mid)add(ls,l,mid,L,R,k);
    if(R>mid)add(rs,mid+1,r,L,R,k);
    summ[u]=(summ[ls]+summ[rs])%mod;
}
void mul(int u,int l,int r,int L,int R,int k){
    if(L<=l&&R>=r){
        summ[u]=1ll*summ[u]*k%mod;
        tmg[u]=1ll*tmg[u]*k%mod;
        tag[u]=1ll*tag[u]*k%mod;
        return;
    }
    push_down(u,l,r);
    if(L<=mid)mul(ls,l,mid,L,R,k);
    if(R>mid)mul(rs,mid+1,r,L,R,k);
    summ[u]=(summ[ls]+summ[rs])%mod;
}
int query(int u,int l,int r,int L,int R){
    if(L<=l&&R>=r)return summ[u];
    push_down(u,l,r);
    if(L>mid)return query(rs,mid+1,r,L,R);
    if(R<=mid)return query(ls,l,mid,L,R);
    return (query(ls,l,mid,L,R)+query(rs,mid+1,r,L,R))%mod;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
	cin>>n>>q>>mod; 
    for(int i=1;i<=n;i++)cin>>a[i];
    build(1,1,n);
    for(int i=1;i<=q;i++){
        cin>>op;
        if(op==1){ 
            cin>>x>>y>>z;
            mul(1,1,n,x,y,z%mod);
        }else if(op==2){
            cin>>x>>y>>z;
            add(1,1,n,x,y,z%mod);
        }else{
            cin>>x>>y;
            cout<<query(1,1,n,x,y)<<"\n";
        }
    }
	return 0;
}

**P10814 离线二维数点

P3370 字符串哈希

字符串哈希:把字符串转化成一个数字,使得相同的字符串的数字一定相同,不相同的字符串数字大概率不同。

我们设长度为n的字符串s的哈希值为 \(s[1]\times base^{n-1}+s[2]\times base^{n-2}+...+s[n] \mod mod\),base通常设置为较小的指数(例如7,23等),mod通常是较大指数(例如1e9+7),这样可以减少哈希冲头,就是不同字符串对应数字相同。

计算哈希值,可以遍历这个字符串,每加入一个字符串把哈希值乘上base再加上新字符。

#include<bits/stdc++.h>
using namespace std;
int n,base=23,mod=1e9+7;
int a[10005];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        string s;
        cin>>s;
        int hash=0;
        for(int j=0;j<s.length();j++){
            hash=(1ll*hash*base+s[j])%mod;
        }
        a[i]=hash;
    }
    sort(a+1,a+n+1);
    int ans=0;
    for(int i=1;i<=n;i++){
        if(a[i]!=a[i-1])ans++;
    }
    cout<<ans;
    return 0;
}

P3375 KMP

用 nxt 数组存储 s2 前 i 位的 botder 值(最长公共前后缀),再匹配过程中,若在一位匹配失败,不必像暴力枚举那样重置 s2 的指针,而是可以跳过前 \(nxt_i\) 个字符,示例如下:

初始状态,前 4 个字符都能匹配,在第 5 个时出现了差异,
需重置 s2 的指针,但可以跳过 nxt[4]=2 个字符。
            ↓
s1  a b a b a b c
            ↓
s2  a b a b c

nxt 0 0 1 2 0

            ↓
s1  a b a b a b c
        . . ↓
s2      a b a b c
          .
nxt 0 0 1 2 0

继续匹配,直到 s2 指针走完整个 s2。

在代码中,i 为 s1 要匹配的字符(每次新加入的),j 为 s2 指针,每次尝试把 s2 中 j+1 位置的字符与要匹配的字符比较,如果不同就匹配失败,只能把 j 退回至跳过 \(nxt_j\) 的位置,成功的话就可以增加 j,更进一步,直到 \(j=len(s2)\) 时即为匹配成功,首个字符位置是 \(i-len(s2)+1\)。时间复杂度 \(O(n)\)。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10; 
char s1[N],s2[N];
int n1,n2;
int nx[N];
int main(){
	scanf("%s\n%s",s1+1,s2+1);
	n1=strlen(s1+1);
    n2=strlen(s2+1);
	for(int i=2,j=0;i<=n2;i++){
        while(j&&s2[i]!=s2[j+1])j=nx[j];
        if(s2[i]==s2[j+1])j++;
        nx[i]=j;
    }
    for(int i=1,j=0;i<=n1;i++){
        while(j&&s1[i]!=s2[j+1])j=nx[j];
        if(s1[i]==s2[j+1])j++;
        if(j==n2){
            cout<<i-n2+1<<endl;
            j=nx[j];
        }
    }
    for(int i=1;i<=n2;i++)cout<<nx[i]<<" ";
	return 0; 
}

P8306 字典树

字典树用于存储字符串集合,树的根节点的孩子连接所有字符串的第一个字符,之后连接第二个字符,例如:字符串集合a,i,ac,abc,int存储如下:(对每个节点标记有多少个字符串经过这个点)

image-20251023212617794

对于本题,创建所有模式串集合的字典树,匹配每个文本串时,到字典树去找,答案为最终点有多少个模式串经过。

#include<bits/stdc++.h>
using namespace std;
const int N=3e6+10;
int t,n,q,num;
struct tree{
    int nx[80];
    int cnt;
}tr[N];
void insert(string s){
    int now=0;
    for(int i=0;i<s.length();i++){
        if(!tr[now].nx[s[i]-'0'])tr[now].nx[s[i]-'0']=++num;
        now=tr[now].nx[s[i]-'0'];
        tr[now].cnt++;
    }
}
int query(string s){
    int now=0;
    for(int i=0;i<s.length();i++){
        if(!tr[now].nx[s[i]-'0'])return 0;
        now=tr[now].nx[s[i]-'0'];
    }
    return tr[now].cnt;
}
int main(){
    cin>>t;
    while(t--){
        cin>>n>>q;
        for(int i=0;i<=num;i++){
            for(int j=0;j<80;j++)tr[i].nx[j]=0;
            tr[i].cnt=0;
        }
        num=0;
        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<<query(s)<<"\n";
        }
    }
    return 0;
}

P3387 缩点

强连通分量(SCC)

定义:有向图中极大的子图,满足子图中的任意两点之间相互可达。

求法:Tarjan算法

在 dfs 搜索时,维护:

  • dfn[u],结点 u 的 dfs 序
  • low[u],表示结点 u 和以结点 u 为子树的结点,经过一条返祖边或横叉边所能到达 dfn 最小的点的 dfn,例如样例中的结点 5,以结点 5 为跟的子树中存在结点 6,经过一天横叉边指向了 4,所有 \(low[5]=4\),样例中 low 数组构造完成后如下:1 1 3 1 4 4 5 5 9

考虑 low 如何计算:

dfs 每一条路线,如果走到的点没有被处理过,那么向下递归,更新 low:low[u]=min(low[u],low[v])。(因为这样是满足 low 数组定义的)

我们需要开一个搜索栈,并记录每个结点是否在栈中。

在搜索时,如果搜到的点在栈中,那么更新 low[u]=min(low[u],dfn[v])。(注意不能写成 low[u]=min(low[u],low[v]),虽然这暂时是可以通过的)

在 dfs 过程中,遇到一个强连通分量的第一个点,具有性质:dfn[x]==low[x]。

dfn[x]==low[x] 表示从 x 的子树出发无法回到 x 的祖先,子树内部形成封闭的强连通分量。此时把栈中从 x 到栈顶的所有点弹出,就找到了一个强连通分量。

时间复杂度 \(O(n+m)\)。

void tarjan(int u){
    dfn[u]=low[u]=++ind;          // 初始化时间戳和追溯值
    s[++top]=u; ins[u]=1;         // 入栈,标记在栈中
    for(int v:g[u]){              // 遍历邻接点
        if(!dfn[v]){              // 未访问:树边
            tarjan(v);            // 递归搜索
            low[u]=min(low[u],low[v]); // 用子节点low更新自己
        }else if(ins[v]){         // 已访问且在栈中:返祖边/横叉边
            low[u]=min(low[u],dfn[v]); // 用dfn[v]更新(不可用low[v])
        }
    }
    if(low[u]==dfn[u]){           // u是SCC的根
        cnt++;                    // 新SCC编号
        int now;
        do{                       // 弹栈,直到弹出u
            now=s[top];top--;
            ins[now]=0;           // 标记出栈
            sum[cnt]+=a[now];     // 累加点权到SCC
            scc[now]=cnt;         // 记录归属
        }while(now!=u);
    }
}

强连通分量缩点

缩点后变成:DAG(有向无环图)这样可以便于拓扑排序、DP。

做法:枚举所有边,如果边连接的是两个不同的强连通分量,就建一条边进行连接,这样又可能出现重边,但是大多情况下不需要去重。

void solve(){
	for(int i=1;i<=n;i++){
		for(int j:g[i]){
			if(scc[i]!=scc[j]){
				new_g[scc[i]].push_back(scc[j]);
			}
		}
	}
}

本题做法

先按上述说的缩点,注意在求的时候要记录每个连通分量的权值和,也就是之后点的全职,将图变成一个 DAG,考虑如何 DP。

记 \(dp[i]\) 为从点 i 出发的一条路径,获得的最大权值和。

每次从一个点开始 dfs,更新 dp 值,最后取 max 即可。

时间复杂度 \(O(n^2)\)。

// 强连通分量
#include<bits/stdc++.h>
using namespace std;
int n,m,a[10005];
vector<int>g[10005];
int dfn[10005],low[10005],s[10005],ins[10005],sum[10005],scc[10005];
int ind,top,cnt;
vector<int>new_g[10005];
int dp[10005];
void tarjan(int u){
	dfn[u]=low[u]=++ind;
	s[++top]=u;ins[u]=1;
	for(int v:g[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}else if(ins[v]){
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(low[u]==dfn[u]){
		int now=-1;
		cnt++;
		while(now!=u){
			now=s[top];top--;
			ins[now]=0;
			sum[cnt]+=a[now];
			scc[now]=cnt;
		}
	}
}
void solve(){
	for(int i=1;i<=n;i++){
		for(int j:g[i]){
			if(scc[i]!=scc[j]){
				new_g[scc[i]].push_back(scc[j]);
			}
		}
	}
}
int dfs(int u){
	if(dp[u]!=-1)return dp[u];
	dp[u]=sum[u];
	for(int v:new_g[u]){
		dp[u]=max(dp[u],dfs(v)+sum[u]);
	}
	return dp[u];
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		g[u].push_back(v);
	}
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
	for(int i=1;i<=n;i++)cout<<low[i]<<" ";
	solve();
	memset(dp,-1,sizeof dp);
	int ans=0;
	for(int i=1;i<=cnt;i++){
		ans=max(ans,dfs(i));
	}
	cout<<ans;
	return 0;
}

*P3388 割点

请先阅读 P8435 点双连通分量。

割点:删去该点后,图不再连通。此时 u 即为割点(非根节点直接标记,根节点需 child >= 2)。

void tarjan(int u,int fa){
    dfn[u]=low[u]=++ind;       // 初始化时间戳和追溯值
    s[++top]=u;                // 当前点入栈
	int child=0;               // 统计根节点的子树数量(用于根割点判断)
    for(int v:g[u]){
        if(v==fa)continue;             // 跳过父节点(无向图防回头)
        if(!dfn[v]){
			child++;                   // 子树计数
            tarjan(v,u);
            low[u]=min(low[u],low[v]); // 用子节点low更新自己
            if(low[v]>=dfn[u]){        // 割点判定:v子树回不到u的祖先
				if(fa!=0)cut[u]=1;     // 非根节点标记割点
                cnt++;                 // 新点双分量编号
                int now;
                do{
                    now=s[top--];
                    dcc[cnt].push_back(now);
                }while(now!=v);        // 弹栈到v为止(u不出栈)
                dcc[cnt].push_back(u); // u也属于该点双,但保留在栈中
            }
        }
        else low[u]=min(low[u],dfn[v]); // 回边更新low
    }
	if(fa==0&&child>=2)cut[u]=1;      // 根节点且有≥2个子树 → 割点
	if(fa==0&&child==0){              // 孤立点(根无子)
        cnt++;
        dcc[cnt].push_back(u);
        top--;
    }
}
```

P8435 点双连通分量

点双连通分量(V-DCC)

定义:无向图中极大的子图,满足删掉任意一个点后,剩余部分仍然连通。

求法:与 SCC 类似,但无向图需跳过父节点(v==fa)。

判定条件:low[v]>=dfn[u](等于也成立,因为删掉 u 后,v 即使能回到 u 也失去意义)

弹栈时,从栈中弹出直到 v,u 也加入当前点双但不出栈(割点可属于多个分量)。

单独一条边连接两个点也是一个点双。孤立点需特殊处理。

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,m;
vector<int>g[N];
int dfn[N],low[N],s[N];
int ind,top,cnt;
vector<int>dcc[N];
void tarjan(int u,int fa){
    dfn[u]=low[u]=++ind;
    s[++top]=u;
	int flag=1;
    for(int v:g[u]){
        if(v==fa) continue;
        if(!dfn[v]){
			flag=0;
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u]){
                cnt++;
                int now;
                do{
                    now=s[top--];
                    dcc[cnt].push_back(now);
                }while(now!=v);
                dcc[cnt].push_back(u);
            }
        }
        else low[u]=min(low[u],dfn[v]);
    }
	if(fa==0&&flag){
        cnt++;
        dcc[cnt].push_back(u);
        top--;
    }
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
	cout<<cnt<<"\n";
	for(int i=1;i<=cnt;i++){
		cout<<dcc[i].size()<<" ";
		for(int j:dcc[i])cout<<j<<" ";
		cout<<"\n";
	}
	return 0;
}

P8436 边双连通分量

边双连通分量(E-DCC)

定义:无向图中极大的子图,满足删掉任意一条边后,剩余部分仍然连通。

求法:先找桥,再染色(不走桥即可)。

判定条件:low[v] > dfn[u](严格大于,若等于则存在重边使边不为桥)

割边(桥):删去该边后,图不再连通。

建图时 tot=1,边成对存储,用 id ^ 1 取反向边。fa_edge 传边编号,用 id == (fa_edge ^ 1) 跳过回头边(可正确处理重边)。

void tarjan(int u,int fa_edge){     // fa_edge是进入u的边编号
    dfn[u]=low[u]=++ind;
    for(auto[v,id]:g[u]){           // 遍历邻接边
        if(id==(fa_edge^1))continue;// 跳过刚走过的反向边(不走回头路)
        if(v==u)continue;           // 跳过自环(自环不影响边双)
        if(!dfn[v]){                // 子节点未访问
            tarjan(v,id);           // 递归搜索
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])       // 桥判定:v子树回不到u(严格大于)
                bridge[id]=bridge[id^1]=1; // 标记正反两条边为桥
        }
        else low[u]=min(low[u],dfn[v]); // 回边更新low
    }
}
void dfs(int u,int c){              // 染色划分边双(不经过桥)
    col[u]=c;                       // 标记当前点属于第c个边双
    dcc[c].push_back(u);            // 加入该分量
    for(auto[v,id]:g[u]){
        if(bridge[id])continue;     // 跳过桥(桥是分量分界线)
        if(v==u)continue;           // 跳过自环
        if(!col[v])dfs(v,c);        // 递归染色未访问的邻点
    }
}

边双缩点后,会变成一棵树(森林)

代码如下:

for(int u=1;u<=n;u++){
    for(auto[v,id]:g[u]){
        if(bridge[id]&&col[u]!=col[v]){
            tr[col[u]].push_back(col[v]);
            tr[col[v]].push_back(col[u]);
        }
    }
}

本题代码

#include<bits/stdc++.h>
using namespace std;
const int N=500005,M=4000005;
int n,m;
vector<pair<int,int>>g[N];
int dfn[N],low[N],ind;
bool bridge[M];
int col[N];
int tot=1;
vector<int>dcc[N];

void tarjan(int u,int fa_edge){
    dfn[u]=low[u]=++ind;
    for(auto[v,id]:g[u]){
        if(id==(fa_edge^1))continue;
        if(v==u)continue;
        if(!dfn[v]){
            tarjan(v,id);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])bridge[id]=bridge[id^1]=1;
        }
        else low[u]=min(low[u],dfn[v]);
    }
}

void dfs(int u,int c){
    col[u]=c;
    dcc[c].push_back(u);
    for(auto[v,id]:g[u]){
        if(bridge[id])continue;
        if(v==u)continue;
        if(!col[v])dfs(v,c);
    }
}

int main(){
    ios::sync_with_stdio(0);
	cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;cin>>u>>v;
        g[u].push_back({v,++tot});
        g[v].push_back({u,++tot});
    }
    for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
    int cnt=0;
    for(int i=1;i<=n;i++)if(!col[i])dfs(i,++cnt);
    cout<<cnt<<"\n";
    for(int i=1;i<=cnt;i++){
        cout<<dcc[i].size()<<" ";
        for(int j:dcc[i])cout<<j<<" ";
        cout<<"\n";
    }
    return 0;
}

P7771 欧拉路径

定义:

  • 欧拉路径:经过图中每条边恰好一次的路径(点可以重复经过)。
  • 欧拉回路:起点和终点相同的欧拉路径(也是每条边恰好一次)。

存在条件:

  • 图必须连通。
  • 对于有向图,若所有点的入度出度都相同,则存在欧拉回路;如果有两个点,其中一个入度比出度少 1,另一个入度比出度多 1,那么存在一个欧拉路径,这两个点分别为路径的起点和终点。
  • 对于无向图,若所有点的度都为偶数,则存在欧拉回路;若奇点(度数为奇数的点)个数为 0 或 2,那么存在一个欧拉路径,奇点为 2 的点分别为这个路径的起点和终点。

求法:Hierholzer算法

有向图(P7771 【模板】欧拉路径)

从一个度数为奇数的点开始 dfs,每次转移,要把走的边删掉,最后倒序输出。

注意删边的处理,可以开数组记录每个点已经处理了前几条边,跳过它们。(开一个数组 del[u] 记录节点 u 的邻接表当前遍历到的位置,每次访问一条边后,del[u]++,确保每条边只被扫描一次,保证复杂度正确)

如果想得到字典序最小的欧拉序,在 DFS 前将每个点的邻接表按终点编号从小到大排序即可。

void dfs(int u) {
    // del[u] 是节点 u 的邻接表“当前待处理位置”指针(从 0 开始)
    // 循环条件:i 从 del[u] 开始,直到邻接表末尾
    // 每次循环后 i 更新为 del[u] 的新值(因为 del[u] 会在循环体内修改)
    for (int v : g[u]) {
        // 关键:立即将 del[u] 后移一位,表示“这条边已经被尝试过了”
        // 这样无论后续是否递归或跳过,下次循环都不会再访问这条边
        del[u] = i + 1;
        // 递归进入目标节点,继续深度优先搜索
        dfs(v);
    }
    // 当 u 的所有邻接边都处理完后,将 u 压入栈
    // 这是后序入栈,最终栈从栈底到栈顶是逆序的欧拉路径
    st.push(u);
}

无向图(P2731 骑马修栅栏)

与刚刚不同的是,无向图的边有两个方向,我们不知道这条边的另一个方向之前走没走过,所以需要对边进行编号,然后开一个 used 数组,表示每条边是否已经被走过。每次走边的时候要标记正反两个方向的边都走过,有向图有重边的情况也可以这样处理。

void dfs(int u) {
    for (int i = del[u]; i < g[u].size(); i = del[u]) {
        // 取出当前边:g[u][i] 是一个 pair<int,int>,first 是邻接点,second 是边的编号
        int v = g[u][i].first;
        int id = g[u][i].second;
        del[u] = i + 1;
        // 如果这条边已经被使用过(正反任一次),则直接跳过
        if (used[id]) continue;
        // 标记这条无向边的正反两个方向为已使用
        // 无向边在邻接表中存了两份,且编号成对(id 和 id^1 互为反向)
        used[id] = used[id ^ 1] = 1;
        dfs(v);
    }
    st.push(u);
}

本题代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
vector<int>g[N];
int del[N];  //其中 del[u] 表示 G[u][1,2……,del[now]-1] 都已经被标记访问过,下一次要从G[now][del[now]]开始访问。;
stack<int>st;
int ru[N],chu[N];
void dfs(int u){
    for(int i=del[u];i<g[u].size();i=del[u]){
        int v=g[u][i];
        del[u]=i+1;
        dfs(v);
    }
    st.push(u);
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;cin>>u>>v;
        g[u].push_back(v);
        ru[v]++;chu[u]++;
    }
    for(int i=1;i<=n;i++)sort(g[i].begin(),g[i].end());
    int s=1;
    int f1=0,f2=0;
    for(int i=1;i<=n;i++){
        if(ru[i]!=chu[i]){
            if(ru[i]+1==chu[i]){
                if(f1){
                    cout<<"No";
                    return 0;
                }
                s=i;f1=1;
            }else if(chu[i]+1==ru[i]){
                if(f2){
                    cout<<"No";
                    return 0;
                }
                f2=1;
            }else{
                cout<<"No";
                return 0;
            }
        }
    }
    dfs(s);
    while(!st.empty()){
        cout<<st.top()<<" ";
        st.pop();
    }
    return 0;
}

*P3383 线性筛素数

#include <bits/stdc++.h>
using namespace std;
const int N=1e8+5;
bool flag[N];
vector<int>st;
int main(){
    ios::sync_with_stdio(0);
    cin.tie();
    int n,q,x,top=0;
    cin>>n>>q;
    for(int i=2;i<=n;i++){
        if(!flag[i]){
            st.push_back(i);
        }
        for(int k=0;k<st.size()&&i*st[k]<=n;k++){
            flag[st[k]*i]=1;
            if(i%st[k]==0)break;
        }
    }
    while(q--){
        cin>>x;
        cout<<st[x-1]<<"\n";
    }
    return 0;
}

*P3811 模意义下的乘法逆元

#include<bits/stdc++.h>
#define int long long
using namespace std;
int inv[3000005];
int n,p;
signed main(){
    ios::sync_with_stdio(0);
	cin.tie(0);
    cin>>n>>p;
    inv[1]=1;
    for(int i=2;i<=n;i++){
        inv[i]=1ll*(p-p/i)*inv[p%i]%p;
    }
    for(int i=1;i<=n;i++){
        cout<<inv[i]<<"\n";
    }
    return 0;
}

*P1495 中国剩余定理

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=15;
int n,a[N],b[N];
pair<int,int>exgcd(int a,int b){
	if(b==0) return{1,0};
	auto[x,y]=exgcd(b,a%b);
	return {y,x-a/b*y};
}
int inv(int a,int p){
	auto[x,y]=exgcd(a,p);
	return(x+p)%p;
}
int CRT(int n,int*a,int*b){
	int M=1,x=0;
	for(int i=1;i<=n;i++) M*=a[i];
	for(int i=1;i<=n;i++){
		__int128 Mi=M/a[i],Ti=inv(Mi,a[i]);
		x=(x+Mi*Ti*b[i]%M)%M;
	}
	return x;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i]>>b[i];
	cout<<CRT(n,a,b);
	return 0;
}

*P3390 矩阵快速幂

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=1e9+7;
int n,m,k;
struct matrix{
    int a[105][105];
    friend matrix operator*(matrix x,matrix y){
        matrix z;
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                z.a[i][j]=0;
                for(int k=1;k<=n;k++){
                    z.a[i][j]=(z.a[i][j]+x.a[i][k]*y.a[k][j]%mod)%mod;
                }
            }
        }
        return z;
    }
}x,y,z;
matrix ksm(matrix x,int kk){
    matrix ans;
    memset(ans.a,0,sizeof(ans.a));
    for(int i=1;i<=n;i++)ans.a[i][i]=1;
    while(kk){
        if(kk&1)ans=ans*x;
        x=x*x;
        kk/=2;
    }
    return ans;
}
signed main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>x.a[i][j];
    z=ksm(x,k);
    for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)cout<<z.a[i][j]<<" ";cout<<"\n";}
    return 0;
}
posted @ 2026-07-25 11:31  郑浩崎  阅读(5)  评论(0)    收藏  举报