图论
链式向前星
struct LS{
int cnt=0;
struct cpp {
int to,cost,next;
}edge[N*2];
int head[ N ];
void inta(int n){
cnt=0;
memset( head,0,sizeof(int)*(n+3) );
}
void addEdge(int from,int to,int cost){
++cnt;
edge[cnt].to = to;
edge[cnt].cost = cost;
edge[cnt].next = head[from];
head[from] =cnt;
}
};
int main(){
int x=1;
for(int i=head[x];i!=0;i=edge[i].next){//遍历
}
return 0;
}
最短路
floyed
void floyed(){
for(int i=1;i<=n;i++){//初始化
for(int j=1;j<=n;j++){
dis[i][j]=1e9;
if(i==j) dis[i][j]=0;
}
}
for(int i=1,a,b,cost;i<=m;i++){
cin>>a>>b>>cost;
dis[a][b]=dis[b][a]=cost;
path[a][b]=b;
path[b][a]=a;
}
//核心代码
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if( dis[i][j]>dis[i][k]+dis[k][j] ){
dis[i][j]=dis[i][k]+dis[k][j];
path[i][j]=path[i][k];//路径
}else if(dis[i][j]==dis[i][k]+dis[k][j]+b[k] && path[i][j]>path[i][k]){//字典序最小路径
path[i][j]=path[i][k];
}
}
}
}
//路径打印
int s,g;
for(int i=s;i!=g;i=path[i][g])
printf("%d ",i);
printf("%d",g);
}
floyed 求最小环
a n s = f k − 1 ( i , j ) + Graph [ i ] [ k ] + Graph [ j ] [ k ] a n s=f^{k-1}(i, j)+\operatorname{Graph}[i][k]+\operatorname{Graph}[j][k] ans=fk−1(i,j)+Graph[i][k]+Graph[j][k]
最小三条边的环
void folyed(){
rep(k,1,n){
rep(i,1,k-1){
rep(j,i+1,k-1){
if(ans>dis[i][j]+cost[i][k]+cost[j][k]){
ans=dis[i][j]+cost[i][k]+cost[j][k];
}
}
}
rep(i,1,n){
rep(j,1,n){
if(dis[i][j]>dis[i][k]+dis[k][j]){
dis[i][j]=dis[i][k]+dis[k][j];
}
}
}
}
}
Dijkstra
struct cpp{
int to,cost;
}tem;
bool vis[10005];
int dis[10005];
int path[10005];
priority_queue<cpp> dui;
bool operator<(const cpp&a,const cpp&b){
return a.cost>b.cost;
}
vector<cpp> g[10005];
int n,s;
void Dijkstra(){
rep(i,1,n) dis[i]=1e9;
dis[s]=0;
tem.cost=0;tem.to=s;
dui.push(tem);
int u;
while(!dui.empty()){
u=dui.top().to;
dui.pop();
if(vis[u]) continue;
vis[u]=1;
rep(i,0, (int)(g[u].size()-1) ){
if( vis[g[u][i].to]==0&&dis[g[u][i].to]>dis[u]+g[u][i].cost ){
dis[g[u][i].to]=dis[u]+g[u][i].cost;
path[g[u][i].to]=u;//更新路径
tem.to=g[u][i].to;tem.cost=dis[g[u][i].to];
dui.push(tem);
}
}
}
}
void print_path(int ed){
stack<int> mystack;
mystack.push(ed);
while(ed!=s){
ed=path[ed];
mystack.push(ed);
}
while(!mystack.empty()){
cout<<mystack.top()<<" ";
mystack.pop();
}
putchar('\n');
}
vector< pair<double,int> > g[N];
bool vis[N];
double dis[N];
priority_queue< pair<double,int>,vector<pair<double,int>>,greater< pair<double,int> > > dui;
void Dijkstra(int s){
for(int i=1;i<=n;i++) dis[i]=1e18,vis[i]=0;
dis[s]=0.0; dui.push( make_pair(0.0,s) );
int u;
while( dui.size() ){
u=dui.top().second;
dui.pop();
if( vis[u] ) continue;
vis[u]=1;
for(auto [y,x]:g[u]){
if( vis[x]==0 && dis[x]>dis[u]+y ){
dis[x]=dis[u]+y;
dui.push( make_pair( dis[x] ,x ) );
}
}
}
}
Bellman_ford
核心思想 : 俩点之间最多有n-1条边
struct cpp{
int from,to,cost;//如果是无向图,记得存from和to交换位置再存一遍
}edge[500005];
int dis[10005];
int path[10005];//路径打印
int n,s,m;
void Bellman_ford(){
rep(i,1,n) dis[i]=1e9;
dis[s]=0;
bool flag;
rep(i,1,n-1){
flag=1;
rep(j,1,m){
if(dis[edge[j].to]>dis[edge[j].from]+edge[j].cost){
dis[edge[j].to]=dis[edge[j].from]+edge[j].cost;
path[edge[j].to]=edge[j].from;//更新路径
flag=0;
}
}
if(flag) break;
}
}
void print_path(int ed){//打印路径
stack<int> mystack;
mystack.push(ed);
while(ed!=s){
ed=path[ed];
mystack.push(ed);
}
while(!mystack.empty()){
cout<<mystack.top()<<" ";
mystack.pop();
}
putchar('\n');
}
SPFA
如果要判负环,统计一个点的入队次数就行了,入队次数大于n则存在负环
int n,s;
int dis[10005];
int path[10005];
bool vis[10005];
struct cpp{
int to,cost;
}tem;
vector<cpp> g[10005];
queue<int> dui;
void SPFA(){
repi(i,1,n) dis[i]=2147483647;
dis[s]=0; vis[s]=1; dui.push(s);
while(!dui.empty() ){
int x=dui.front();dui.pop();
vis[x]=0;
repi(i,0, (int) (g[x].size()-1) ){
if( dis[ g[x][i].to ]>dis[ x ] + g[x][i].cost ){
dis[ g[x][i].to ]=dis[ x ] + g[x][i].cost;
path[ g[x][i].to ]=x;//更新路径
if(vis[ g[x][i].to ]==0){
vis[ g[x][i].to ]=1;
dui.push(g[x][i].to);
}
}
}
}
}
void print_path(int ed){//打印路径
stack<int> mystack;
mystack.push(ed);
while(ed!=s){
ed=path[ed];
mystack.push(ed);
}
while(!mystack.empty()){
cout<<mystack.top()<<" ";
mystack.pop();
}
putchar('\n');
}
差分约束
差分约束 - Kersen - 博客园 (cnblogs.com)
最小生成树
Kruskal算法
int fa[10005];
inline int find(int u){
if ( 0==fa[u]) return fa[u]=u;
int x=u,t;
while(x!=fa[x]) x=fa[x];
while(u!=x){ t=fa[u];fa[u]=x;u=t; }
return x;
}
inline void un(int u,int v){
fa[find(u)]=find(v);
}
inline bool same_r(int u,int v){
return find(u)==find(v);
}
struct cpp{
int a,b,c;
}edge[200005];
bool cmp(cpp &x,cpp &y){
return x.c<y.c;
}
int Kruskal( int id,int len,int ge ){//边开始下标,总边个数,需要生成边个数
sort( edge+id,edge+id+len,cmp );
int ans=0;
repi(i,0,len-1){
if( !same_r( edge[i].a,edge[i].b ) ){
un( edge[i].a,edge[i].b );
ans+=edge[i].c;
ge--;
if(0==ge) return ans;
}
}
return -1;
}
int ans;
int n,m;
int main(){
scanf("%d %d",&n,&m);
for(int i=0;i<m;i++)
scanf("%d %d %d",&edge[i].a,&edge[i].b,&edge[i].c);
cout<<Kruskal( 0,m,n-1 );
return 0;
}
Prim算法
T(n^2)
struct cpp{
int x,cost;//到达的点和消耗
}tem;
vector<cpp> g[5005];
bool vis[5005];
int num=1;
int dis[5005];
int n,m;
inline int prim(){
int sum=0,min_cost,cnt=0;
while(++cnt<n){
min_cost=2e9;
for(unsigned int i=0;i<g[num].size();i++){
dis[g[num][i].x]=min(dis[g[num][i].x],g[num][i].cost);
}
vis[num]=1;
for(int i=1;i<=n;i++){
if(vis[i]==0 && min_cost>dis[i]){
num=i;
min_cost=dis[i];
}
}
sum+=min_cost;
}
return sum;
}
int main(){
cin>>n>>m;
for(int i=2;i<=n;i++)
dis[i]=2e9;
for(int i=0,a;i<m;i++){
cin>>a>>tem.x>>tem.cost;
g[a].push_back(tem);
swap(a,tem.x);
g[a].push_back(tem);
}
cout<<prim();
return 0;
}
次小生成树
核心思想 : 枚举最小生成树总未被利用的边 , 会生成一个环 , 删除环中的除自己以外的最大边;
新树权值=MST权值+枚举边权值 - 环最大权值
次小生成树权值=所有新树的的最小权值树
Kruskal重构树
性质:
这是一个二叉大根堆
原树两点之间的边权最大值是重构树上两点Lca的权值
重构树中代表原树中的点的节点全是叶子节点,其余节点都代表了一条边的边权
ll a[N];
struct E{
int u,v; ll w;
}e[N];
vector<int> g[N];
void Ex_Kruskal(){
int id=n;
sort(e+1,e+1+m,[](const E &A,const E &B){
return A.w<B.w;
});
for(int i=1;i<=n*2;i++) fa[i]=i;
for(int i=1,u,v;i<=m;i++){
u=find(e[i].u); v=find(e[i].v);
if(u==v) continue;
fa[u]=fa[v]=++id;
a[id]=e[i].w;//边权变点权
g[id].push_back(u);
g[id].push_back(v);
g[u].push_back(id);
g[v].push_back(id);
if(id==2*n-1) break;
}
}
二分图
二分图的判定 : 染色法
匈牙利
int n,m,e;
vector<int> g[N];
bool vis[N];
int nxt[N];
bool Find(int id){
for( auto x:g[id] ){
if( vis[x] ) continue;
vis[x] = 1;
if( nxt[x]==0 || Find(nxt[x]) ){
nxt[x] = id;
return true;
}
}
return false;
}
int match(){
int sum=0;
for(int i=1;i<=n;++i){
ms(vis,0);
if( Find(i) ) ++sum;
}
return sum;
}
int main(){
intxt();
cin>>n>>m>>e;
int u,v;
rep(i,1,e){
cin>>u>>v;
g[u].pb(v);
}
cout<<match()<<endl;
return 0;
}
网络流
Dinic
const int N=2e2+15;//点数
const int M=5e3+15;//边数
struct DINIC{
int n,m,s,t;
ll tot=0;
bool vis[N];
int dep[N],q[N<<1],cur[N],l,r;
struct LS{
int cnt=1;
int to[M<<1],nxt[M<<1];
ll val[M<<1];
int head[N];
void inta(int n){
cnt = 1;
memset( head,0,sizeof(int)*(n+3) );
}
void addEdge(int u,int v,ll w){
++cnt;
to[cnt] = v;
val[cnt] = w;
nxt[cnt] = head[u];
head[u] = cnt;
}
}G;
bool bfs(){
memset(dep , 0 , sizeof(int)*(n+2) );
q[l=r=1] = s;
dep[s] = 1;
while(l<=r){//队列
int u = q[l++];
for(int p=G.head[u];p;p=G.nxt[p]){
int v = G.to[p];
if( !G.val[p] || dep[v] )
continue;
dep[v] = dep[u] + 1;
q[++r] = v;
}
}
return dep[t]!=0;
}
ll dfs(int u,ll in){
if(u==t) return in;
ll out = 0;
for(int &p=cur[u]; p&&in ;p=G.nxt[p]){
int v = G.to[p];
if( dep[v]==dep[u]+1 && G.val[p]!=0LL ){
ll res = dfs( v,min( G.val[p],in ) );
G.val[p] -= res;
G.val[p^1] += res;
in -= res;
out += res;
if( in==0LL ) return out;
}
}
if(out == 0LL)
dep[u] = 0LL;
return out;
}
ll Dinic(){
scanf("%d%d%d%d",&n,&m,&s,&t);
G.inta(n);
int u,v;ll w;
while(m--){
scanf("%d%d%lld",&u,&v,&w);
G.addEdge(u,v,w);
G.addEdge(v,u,0LL);
}
while(bfs()){
memcpy(cur, G.head, sizeof(int)*(n+2) );
tot += dfs(s,1e18);
}
return tot;
}
}D;
int main(){
intxt();
printf("%lld\n",D.Dinic());
return 0;
}
结论
七桥问题
图是否可以从某点开始经过每一条边准确一次 : (边全联通)只要满足图每个点的度全为偶或者刚好两个奇则满足
其中全为偶时 , 每个点都可以作为起点 , 起点就是终点
刚好两个奇时 , 两个奇分别为起点或终点
树
DFS序
每个节点在dfs深度优先遍历中的进出栈的时间序列。
定义两个数组,in[x],out[x]。dfs从根结点开始,每个结点分别记录两个信息:in[x],out[x],in[x]为dfs进入结点x时的时间戳,out[x]为dfs离开结点x时的时间戳。
那么节点x可以管辖到的子树范围就是[ in[x] , out[x] ]
, 这是就可以用维护区间的数据结构来操作了 , 线段树,树状数组等
int time=0;
int in[maxn];
int out[maxn];
void dfs( int id ){
in[id]=++time;
for(int i=head[id];i!=-1;i=edge[i].next){
dfs( edge[i].to );
}
out[id]=time;
}
树的直径
树的直径 : 树中距离最远的两个点
求法 : 跑两遍bfs或者dfs即可
1,任取一个点,找到离这这个点最远的节点
2,求该节点求到其他点的距离,其中的最长距离就是树的直径
树的重心
树的重心定义为树的某个节点,当去掉该节点后,树的各个连通分量中,节点数最多的连通分量其节点数达到最小值。性质 : 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
int weigh[maxn];//weight最小的是树的重心
bool vis[maxn];
int dfs( int id ){
int res=0,sum=1,tem;
for(int i=head[id];i!=-1;i=edge[i].next){
if( vis[ edge[i].to ]==0 ){
vis[ edge[i].to ]=1;
tem=dfs( edge[i].to );
res=max( tem,res );
sum+=tem;
}
}
weigh[id]=max( res,n-sum );
return sum;
}
最近公共祖先LCA
树剖法
struct LS{
int cnt=0;
struct cpp {
int to,cost,next;
}edge[N*2];
int head[ N ];
void inta(){
cnt=0;
memset( head,-1,sizeof(head) );
}
void addEdge(int from,int to,int cost){
++cnt;
edge[cnt].to = to;
edge[cnt].cost = cost;
edge[cnt].next = head[from];
head[from] =cnt;
}
}G;
int n,m,root;
struct T{
int fa,dep,size,son,top;
}node[N];
void dfs1(int u,int f,int depth){
node[u].fa = f ;
node[u].dep = depth ;
node[u].size = 1 ;
for(int i=G.head[u];i!=-1;i=G.edge[i].next){
int v=G.edge[i].to;
if(v==f) continue;
dfs1(v,u,depth+1);
node[u].size += node[v].size;
if( node[v].size > node[node[u].son].size ) node[u].son = v;
}
}
void dfs2(int u,int t){
node[u].top = t ;
if( !node[u].son ) return;
dfs2( node[u].son,t );
for(int i=G.head[u];i!=-1;i=G.edge[i].next){
int v=G.edge[i].to;
if( v!=node[u].son && v!=node[u].fa )
dfs2(v,v);
}
}
int lca_query(int x,int y){
while( node[x].top!=node[y].top ){
if( node[ node[x].top ].dep < node[ node[y].top ].dep ){
swap(x,y);
}
x = node[ node[ x ].top ].fa ;
}
return node[x].dep<node[y].dep?x:y;
}
int main(){
intxt();
read(n);read(m);read(root);
G.inta();
int u,v;
rep(i,1,n-1){
read(u);read(v);
G.addEdge(u,v,0);
G.addEdge(v,u,0);
}
dfs1(root,-1,1);
dfs2(root,root);
rep(i,1,m){
read(u);read(v);
printf("%d\n",lca_query( u,v ));
}
return 0;
}
倍增法
int n,s;
int depth[N];
int anc[N][30];
void dfs(int u,int fa){//深搜出各点的深度 , 存在depth中
anc[u][0]=fa;
for(int i=head[u];i!=-1;i=edge[i].next){//遍历
if( edge[i].to==fa ) continue;
depth[edge[i].to] = depth[u] + 1 ;
dfs(edge[i].to,u);
}
}
void bz(){//倍增,处理anc数组
int maxdep=int(log(n)/log(2) + 1);
for(int j=1;j<=maxdep;j++){
for(int i=1;i<=n;i++){//i = 点下标 , n为点结束
anc[i][j] = anc[ anc[i][j-1] ][j-1];//初始化全部赋值-1
}
}
}
int lca_query(int u,int v){//询问最近公共祖先
if( depth[u]<depth[v] ) swap(u,v);
int logsn = int( log(depth[u])/log(2) + 1);
for(int i = logsn;i>=0;i--){
if( depth[u] - (1<<i)>=depth[v] )
u = anc[u][i];
}
if(u==v) return u;
for(int i=logsn;i>=0;i--){
if( anc[u][i]!=-1 && anc[u][i]!=anc[v][i] ){
u = anc[u][i];
v = anc[v][i];
}
}
return anc[u][0];
}
void init(){
memset(anc,-1,sizeof(anc));
dfs(s,-1);//s为根
bz();
}
树上差分
点差分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zk98cqPv-1650207058469)(D:\makedown\ACM_doc\图论\点差分.png)]
点u,v之间的值+val , 那么cnt[u]+=val , cnt[v]+=val , cnt[LCA]-=val , cnt[ fa[LCA] ]-=val;
最后dfs回溯就行num[u]=cnt[u] + all_son( num[] )
边差分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QjSF3EuL-1650207058470)(D:\makedown\ACM_doc\图论\边差分.png)]
把边塞给点的话,是塞给这条边所连的深度较深的节点. (即塞给儿子节点
int cnt[N],num[N];
void dfs1(int u,int fa,int &ans){
num[u] = cnt[u];
for(auto x:g[u]){
if( x.to==fa ) continue;
dfs1(x.to,u,ans);
num[u]+=num[x.to];
}
tarjan
缩点
int n,m,cnt_dfn,cnt_sc;
vector<int> g[N],scg[N];
int dfn[N],low[N],sc_id[N],sc_nums[N];
bool in_sta[N];
stack<int> sta;
void tarjan(int u,int f){
low[u]=dfn[u]=++cnt_dfn;
sta.push(u); in_sta[u]=1;
for(auto x:g[u]){
if(dfn[x]==0){
tarjan(x,u);
low[u]=min(low[u],low[x]);
}else if(in_sta[x]==1){
low[u]=min(low[u],dfn[x]);
}
}
if( dfn[u]==low[u] ){
++cnt_sc;
while( sta.top()!=u ){
sc_id[sta.top()]=cnt_sc;
sc_nums[cnt_sc]++;
in_sta[sta.top()]=0;
sta.pop();
}
sc_id[sta.top()]=cnt_sc;
sc_nums[cnt_sc]++;
in_sta[sta.top()]=0;
sta.pop();
}
}
//建立缩点新图
void creat_new_g(){
for(int i=1;i<=n;i++){
if( dfn[i]==0 ) tarjan(i,i);
}
for(int i=1;i<=n;i++){
for(auto x:g[i]){
if( sc_id[i]==sc_id[x] ) continue;
scg[ sc_id[i] ].push_back( sc_id[x] );
}
}
for(int i=1;i<=cnt_sc;i++){
sort( scg[i].begin(),scg[i].end() );
scg[i].erase( unique( scg[i].begin(),scg[i].end() ),scg[i].end() );
}
}
求割点
int n,m,cnt_dfn;
int low[N],dfn[N];
bool vis[N];
vector<int> g[N];
void tarjan(int u,int root){
low[u]=dfn[u]=++cnt_dfn;
int cnt_son=0;
for(auto x:g[u]){
if(low[x]==0){
tarjan(x,root);
low[u]=min(low[u],low[x]);
if(low[x]>=dfn[u]&&u!=root) vis[u]=1;
if(u==root) cnt_son++;
}
low[u]=min(low[u],dfn[x]);
}
if(cnt_son>=2&&u==root){
vis[u]=1;
}
}
int main(){
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1;i<=n;i++){
if(dfn[i]==0) tarjan(i,i);
}
int ans=0;
for(int i=1;i<=n;i++){
if(vis[i]==1) ++ans;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++){
if(vis[i]) cout<<i<<" ";
}
return 0;
}
求割边(桥)
int n,m,cnt_dfn;
vector<int> g[N];
int dfn[N],low[N];
vector<pair<int,int>> bridges;
void tarjan(int u,int f){
low[u]=dfn[u]=++cnt_dfn;
for(auto x:g[u]){
if(dfn[x]==0){
tarjan(x,u);
low[u]=min(low[u],low[x]);
if(low[x]>dfn[u]){
bridges.push_back(make_pair(u,x));
}
}else if(f!=x){
low[u]=min(low[u],dfn[x]);
}
}
}