(待更新)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。
时间复杂度:预处理 \(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)。
时间复杂度:预处理\(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()用于建树。
区间修改需要运用懒标记,每次修改区间,只将对应区间的懒标记增加,之后如果用到这个区间的子节点,就需要将懒标记下放,注意懒标记存储的是区间内每个数应该加的数量,所以计算和的时候要乘上对应序列的长度,如图:
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存储如下:(对每个节点标记有多少个字符串经过这个点)
对于本题,创建所有模式串集合的字典树,匹配每个文本串时,到字典树去找,答案为最终点有多少个模式串经过。
#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;
}

浙公网安备 33010602011771号